Skip to content
108 changes: 108 additions & 0 deletions packages/api-rest/__tests__/apis/common/publicApis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,5 +681,113 @@ 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();
});

it('should override global defaultAuthMode with local defaultAuthMode configuration', async () => {
const mockAmplifyWithGlobalConfig = {
...mockAmplifyInstance,
libraryOptions: {
...mockAmplifyInstance.libraryOptions,
API: {
...mockAmplifyInstance.libraryOptions?.API,
REST: {
defaultAuthMode: 'none' as const,
},
},
},
} as any as AmplifyClassV6;

mockFetchAuthSession.mockClear();
mockFetchAuthSession.mockResolvedValue({ credentials });

await fn(mockAmplifyWithGlobalConfig, {
apiName: 'restApi1',
path: '/private',
options: {
defaultAuthMode: 'iam',
},
}).response;

expect(mockFetchAuthSession).toHaveBeenCalled();
expect(mockAuthenticatedHandler).toHaveBeenCalled();
expect(mockUnauthenticatedHandler).not.toHaveBeenCalled();
});
});
});
});
19 changes: 16 additions & 3 deletions packages/api-rest/src/apis/common/transferHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import {
import {
AWSCredentials,
DocumentType,
RESTAuthMode,
RetryStrategy,
} from '@aws-amplify/core/internals/utils';

import {
logger,
parseRestApiServiceError,
parseSigningInfo,
resolveLibraryOptions,
} from '../../utils';
import { resolveHeaders } from '../../utils/resolveHeaders';
import { RestApiResponse, SigningServiceInfo } from '../../types';
Expand All @@ -30,6 +32,7 @@ type HandlerOptions = Omit<HttpRequest, 'body' | 'headers'> & {
headers?: Headers;
withCredentials?: boolean;
retryStrategy?: RetryStrategy;
defaultAuthMode?: RESTAuthMode;
};

type RetryDecider = RetryOptions['retryDecider'];
Expand Down Expand Up @@ -75,19 +78,29 @@ export const transferHandler = async (
method,
body: resolvedBody,
};
const {
retryStrategy: libraryRetryStrategy,
defaultAuthMode: libraryDefaultAuthMode,
} = resolveLibraryOptions(amplify);
const baseOptions = {
retryDecider: getRetryDeciderFromStrategy(
retryStrategy ?? amplify?.libraryOptions?.API?.REST?.retryStrategy,
retryStrategy ?? libraryRetryStrategy,
),
computeDelay: jitteredBackoff,
withCrossDomainCredentials: withCredentials,
abortSignal,
};

const isIamAuthApplicable = iamAuthApplicable(request, signingServiceInfo);
const defaultAuthMode = options.defaultAuthMode ?? libraryDefaultAuthMode;

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;
/**
* custom timeout in milliseconds.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/api-rest/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export { createCancellableOperation } from './createCancellableOperation';
export { parseSigningInfo } from './parseSigningInfo';
export { parseRestApiServiceError } from './serviceError';
export { resolveApiUrl } from './resolveApiUrl';
export { resolveLibraryOptions } from './resolveLibraryOptions';
export { logger } from './logger';
14 changes: 14 additions & 0 deletions packages/api-rest/src/utils/resolveLibraryOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { AmplifyClassV6 } from '@aws-amplify/core';

/**
* @internal
*/
export const resolveLibraryOptions = (amplify: AmplifyClassV6) => {
const retryStrategy = amplify.libraryOptions?.API?.REST?.retryStrategy;
const defaultAuthMode = amplify.libraryOptions?.API?.REST?.defaultAuthMode;

return { retryStrategy, defaultAuthMode };
};
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;
/**
* custom timeout in milliseconds configurable for given REST service, or/and method.
*/
Expand Down Expand Up @@ -142,6 +146,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