Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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