Skip to content
78 changes: 78 additions & 0 deletions packages/api-rest/__tests__/apis/common/publicApis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
});
1 change: 1 addition & 0 deletions packages/api-rest/src/apis/common/publicApis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const publicHandler = (
method,
headers,
abortSignal,
defaultAuthMode: apiOptions.defaultAuthMode,
},
isIamAuthApplicableForRest,
signingServiceInfo,
Expand Down
14 changes: 12 additions & 2 deletions packages/api-rest/src/apis/common/transferHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {
AWSCredentials,
DocumentType,
RESTAuthMode,
RetryStrategy,
} from '@aws-amplify/core/internals/utils';

Expand All @@ -30,6 +31,7 @@ type HandlerOptions = Omit<HttpRequest, 'body' | 'headers'> & {
headers?: Headers;
withCredentials?: boolean;
retryStrategy?: RetryStrategy;
defaultAuthMode?: RESTAuthMode;
};

type RetryDecider = RetryOptions['retryDecider'];
Expand Down Expand Up @@ -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 =
Expand Down
7 changes: 6 additions & 1 deletion packages/api-rest/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -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<RestApiOptionsBase>;
export type PostInput = ApiInput<RestApiOptionsBase>;
Expand Down Expand Up @@ -41,6 +45,7 @@ export interface RestApiOptionsBase {
* @default ` { strategy: 'jittered-exponential-backoff' } `
*/
retryStrategy?: RetryStrategy;
defaultAuthMode?: RESTAuthMode;
}

type Headers = Record<string, string>;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/libraryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export {
AssociationHasOne,
DocumentType,
GraphQLAuthMode,
RESTAuthMode,
ModelFieldType,
NonModelFieldType,
ModelIntrospectionSchema,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/singleton/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}

Expand Down Expand Up @@ -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.
*/
Expand Down
Loading