diff --git a/packages/api-rest/__tests__/apis/common/publicApis.test.ts b/packages/api-rest/__tests__/apis/common/publicApis.test.ts index 8381fbf8406..1dae35f7bcc 100644 --- a/packages/api-rest/__tests__/apis/common/publicApis.test.ts +++ b/packages/api-rest/__tests__/apis/common/publicApis.test.ts @@ -617,5 +617,83 @@ describe('public APIs', () => { expect(result).toEqual({ retryable: false }); }); }); + + describe('defaultAuthMode option', () => { + it('should skip credential resolution when defaultAuthMode is "none"', async () => { + mockFetchAuthSession.mockClear(); + + await fn(mockAmplifyInstance, { + apiName: 'restApi1', + path: '/public', + options: { + defaultAuthMode: 'none', + }, + }).response; + + expect(mockFetchAuthSession).not.toHaveBeenCalled(); + expect(mockUnauthenticatedHandler).toHaveBeenCalled(); + expect(mockAuthenticatedHandler).not.toHaveBeenCalled(); + }); + + it('should resolve credentials when defaultAuthMode is "iam"', async () => { + mockFetchAuthSession.mockResolvedValue({ + credentials: { + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }, + }); + + await fn(mockAmplifyInstance, { + apiName: 'restApi1', + path: '/private', + options: { + defaultAuthMode: 'iam', + }, + }).response; + + expect(mockFetchAuthSession).toHaveBeenCalled(); + expect(mockAuthenticatedHandler).toHaveBeenCalled(); + }); + + it('should maintain default behavior when no defaultAuthMode specified', async () => { + mockFetchAuthSession.mockResolvedValue({ + credentials: null, + }); + + await fn(mockAmplifyInstance, { + apiName: 'restApi1', + path: '/endpoint', + }).response; + + expect(mockFetchAuthSession).toHaveBeenCalled(); + expect(mockUnauthenticatedHandler).toHaveBeenCalled(); + }); + + it('should use global defaultAuthMode configuration when no local defaultAuthMode is specified', async () => { + const mockAmplifyWithGlobalConfig = { + ...mockAmplifyInstance, + libraryOptions: { + ...mockAmplifyInstance.libraryOptions, + API: { + ...mockAmplifyInstance.libraryOptions?.API, + REST: { + defaultAuthMode: 'none' as const, + }, + }, + }, + } as any as AmplifyClassV6; + + mockFetchAuthSession.mockClear(); + + await fn(mockAmplifyWithGlobalConfig, { + apiName: 'restApi1', + path: '/public', + }).response; + + expect(mockFetchAuthSession).not.toHaveBeenCalled(); + expect(mockUnauthenticatedHandler).toHaveBeenCalled(); + expect(mockAuthenticatedHandler).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/packages/api-rest/src/apis/common/publicApis.ts b/packages/api-rest/src/apis/common/publicApis.ts index d2eaa16e110..24f9411ad2a 100644 --- a/packages/api-rest/src/apis/common/publicApis.ts +++ b/packages/api-rest/src/apis/common/publicApis.ts @@ -71,6 +71,7 @@ const publicHandler = ( method, headers, abortSignal, + defaultAuthMode: apiOptions.defaultAuthMode, }, isIamAuthApplicableForRest, signingServiceInfo, diff --git a/packages/api-rest/src/apis/common/transferHandler.ts b/packages/api-rest/src/apis/common/transferHandler.ts index 84a1d1f799c..c81889c55d4 100644 --- a/packages/api-rest/src/apis/common/transferHandler.ts +++ b/packages/api-rest/src/apis/common/transferHandler.ts @@ -11,6 +11,7 @@ import { import { AWSCredentials, DocumentType, + RESTAuthMode, RetryStrategy, } from '@aws-amplify/core/internals/utils'; @@ -30,6 +31,7 @@ type HandlerOptions = Omit & { headers?: Headers; withCredentials?: boolean; retryStrategy?: RetryStrategy; + defaultAuthMode?: RESTAuthMode; }; type RetryDecider = RetryOptions['retryDecider']; @@ -84,10 +86,18 @@ export const transferHandler = async ( abortSignal, }; - const isIamAuthApplicable = iamAuthApplicable(request, signingServiceInfo); + const defaultAuthMode = + options.defaultAuthMode ?? + amplify?.libraryOptions?.API?.REST?.defaultAuthMode; + + let credentials: AWSCredentials | null = null; + if (defaultAuthMode !== 'none') { + credentials = await resolveCredentials(amplify); + } let response: RestApiResponse; - const credentials = await resolveCredentials(amplify); + const isIamAuthApplicable = iamAuthApplicable(request, signingServiceInfo); + if (isIamAuthApplicable && credentials) { const signingInfoFromUrl = parseSigningInfo(url); const signingService = diff --git a/packages/api-rest/src/types/index.ts b/packages/api-rest/src/types/index.ts index f011f717a94..27d13cbc315 100644 --- a/packages/api-rest/src/types/index.ts +++ b/packages/api-rest/src/types/index.ts @@ -1,6 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { DocumentType, RetryStrategy } from '@aws-amplify/core/internals/utils'; +import { + DocumentType, + RESTAuthMode, + RetryStrategy, +} from '@aws-amplify/core/internals/utils'; export type GetInput = ApiInput; export type PostInput = ApiInput; @@ -41,6 +45,7 @@ export interface RestApiOptionsBase { * @default ` { strategy: 'jittered-exponential-backoff' } ` */ retryStrategy?: RetryStrategy; + defaultAuthMode?: RESTAuthMode; } type Headers = Record; diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index bba46ba7ce9..ea1c6c7c7a5 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -48,6 +48,7 @@ export { AssociationHasOne, DocumentType, GraphQLAuthMode, + RESTAuthMode, ModelFieldType, NonModelFieldType, ModelIntrospectionSchema, diff --git a/packages/core/src/singleton/API/types.ts b/packages/core/src/singleton/API/types.ts index 1fceab2b5c3..26ca13fdc38 100644 --- a/packages/core/src/singleton/API/types.ts +++ b/packages/core/src/singleton/API/types.ts @@ -25,6 +25,10 @@ export interface LibraryAPIOptions { * @default ` { strategy: 'jittered-exponential-backoff' } ` */ retryStrategy?: RetryStrategy; + /** + * Default auth mode for REST API calls when no explicit auth is provided. + */ + defaultAuthMode?: RESTAuthMode; }; } @@ -138,6 +142,8 @@ export type GraphQLAuthMode = | 'lambda' | 'none'; +export type RESTAuthMode = 'none' | 'iam'; + /** * Type representing a plain JavaScript object that can be serialized to JSON. */