From e2824b68a7de2ba229c6ce8e90ea6724c5590b42 Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Wed, 3 Jan 2024 10:27:44 -0800 Subject: [PATCH] fix(api-rest): omit no credentials error and make unauth request(#12736) --- .../apis/common/internalPost.test.ts | 19 +++++++ .../__tests__/apis/common/publicApis.test.ts | 54 ++++++++++--------- packages/api-rest/src/apis/common/handler.ts | 25 +++++++-- packages/api-rest/src/errors/validation.ts | 4 -- packages/api-rest/src/utils/index.ts | 1 - .../api-rest/src/utils/resolveCredentials.ts | 17 ------ 6 files changed, 70 insertions(+), 50 deletions(-) delete mode 100644 packages/api-rest/src/utils/resolveCredentials.ts diff --git a/packages/api-rest/__tests__/apis/common/internalPost.test.ts b/packages/api-rest/__tests__/apis/common/internalPost.test.ts index 32b83ba53cd..133e4c2a4bc 100644 --- a/packages/api-rest/__tests__/apis/common/internalPost.test.ts +++ b/packages/api-rest/__tests__/apis/common/internalPost.test.ts @@ -209,6 +209,25 @@ describe('internal post', () => { expect(mockAuthenticatedHandler).not.toHaveBeenCalled(); }); + it('should call unauthenticatedHandler if credential is not set', async () => { + mockFetchAuthSession.mockClear(); + mockFetchAuthSession.mockRejectedValue( + new Error('Mock error as credentials not configured') + ); + await post(mockAmplifyInstance, { + url: apiGatewayUrl, + }); + expect(mockUnauthenticatedHandler).toHaveBeenCalledWith( + { + url: apiGatewayUrl, + method: 'POST', + headers: {}, + }, + expect.anything() + ); + expect(mockAuthenticatedHandler).not.toHaveBeenCalled(); + }); + it('should abort request when cancel is called', async () => { expect.assertions(4); let underLyingHandlerReject; diff --git a/packages/api-rest/__tests__/apis/common/publicApis.test.ts b/packages/api-rest/__tests__/apis/common/publicApis.test.ts index 82adcea15cf..8abf6a97f43 100644 --- a/packages/api-rest/__tests__/apis/common/publicApis.test.ts +++ b/packages/api-rest/__tests__/apis/common/publicApis.test.ts @@ -4,6 +4,7 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { authenticatedHandler, + unauthenticatedHandler, parseJsonError, } from '@aws-amplify/core/internals/aws-client-utils'; @@ -25,6 +26,7 @@ import { jest.mock('@aws-amplify/core/internals/aws-client-utils'); const mockAuthenticatedHandler = authenticatedHandler as jest.Mock; +const mockUnauthenticatedHandler = unauthenticatedHandler as jest.Mock; const mockFetchAuthSession = jest.fn(); let mockConfig = { API: { @@ -60,6 +62,17 @@ const credentials = { sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', }; +const mockSuccessResponse = { + statusCode: 200, + headers: { + 'response-header': 'response-header-value', + }, + body: { + blob: jest.fn(), + json: jest.fn(), + text: jest.fn(), + }, +}; describe('public APIs', () => { beforeEach(() => { @@ -67,17 +80,9 @@ describe('public APIs', () => { mockFetchAuthSession.mockResolvedValue({ credentials, }); - mockAuthenticatedHandler.mockResolvedValue({ - statusCode: 200, - headers: { - 'response-header': 'response-header-value', - }, - body: { - blob: jest.fn(), - json: jest.fn().mockResolvedValue({ foo: 'bar' }), - text: jest.fn(), - }, - }); + mockSuccessResponse.body.json.mockResolvedValue({ foo: 'bar' }); + mockAuthenticatedHandler.mockResolvedValue(mockSuccessResponse); + mockUnauthenticatedHandler.mockResolvedValue(mockSuccessResponse); mockGetConfig.mockReturnValue(mockConfig); }); const APIs = [ @@ -266,20 +271,21 @@ describe('public APIs', () => { } }); - it('should throw if credentials are not available', async () => { - expect.assertions(2); + it('should use unauthenticated request if credentials are not available', async () => { + expect.assertions(1); mockFetchAuthSession.mockResolvedValueOnce({}); - try { - await fn(mockAmplifyInstance, { - apiName: 'restApi1', - path: '/items', - }).response; - } catch (error) { - expect(error).toBeInstanceOf(RestApiError); - expect(error).toMatchObject( - validationErrorMap[RestApiValidationErrorCode.NoCredentials] - ); - } + await fn(mockAmplifyInstance, { + apiName: 'restApi1', + path: '/items', + }).response; + expect(mockUnauthenticatedHandler).toHaveBeenCalledWith( + expect.objectContaining({ + url: new URL( + 'https://123.execute-api.us-west-2.amazonaws.com/development/items/123' + ), + }), + expect.anything() + ); }); it('should throw when response is not ok', async () => { diff --git a/packages/api-rest/src/apis/common/handler.ts b/packages/api-rest/src/apis/common/handler.ts index 2a7b555dad5..45556f70a8a 100644 --- a/packages/api-rest/src/apis/common/handler.ts +++ b/packages/api-rest/src/apis/common/handler.ts @@ -9,12 +9,15 @@ import { jitteredBackoff, authenticatedHandler, } from '@aws-amplify/core/internals/aws-client-utils'; -import { DocumentType } from '@aws-amplify/core/internals/utils'; +import { + AWSCredentials, + DocumentType, +} from '@aws-amplify/core/internals/utils'; import { + logger, parseRestApiServiceError, parseSigningInfo, - resolveCredentials, } from '../../utils'; import { resolveHeaders } from '../../utils/resolveHeaders'; import { RestApiResponse } from '../../types'; @@ -67,13 +70,13 @@ export const transferHandler = async ( const isIamAuthApplicable = iamAuthApplicable(request, signingServiceInfo); let response: RestApiResponse; - if (isIamAuthApplicable) { + const credentials = await resolveCredentials(amplify); + if (isIamAuthApplicable && credentials) { const signingInfoFromUrl = parseSigningInfo(url); const signingService = signingServiceInfo?.service ?? signingInfoFromUrl.service; const signingRegion = signingServiceInfo?.region ?? signingInfoFromUrl.region; - const credentials = await resolveCredentials(amplify); response = await authenticatedHandler(request, { ...baseOptions, credentials, @@ -97,3 +100,17 @@ const iamAuthApplicable = ( { headers }: HttpRequest, signingServiceInfo?: SigningServiceInfo ) => !headers.authorization && !headers['x-api-key'] && !!signingServiceInfo; + +const resolveCredentials = async ( + amplify: AmplifyClassV6 +): Promise => { + try { + const { credentials } = await amplify.Auth.fetchAuthSession(); + if (credentials) { + return credentials; + } + } catch (e) { + logger.debug('No credentials available, the request will be unsigned.'); + } + return null; +}; diff --git a/packages/api-rest/src/errors/validation.ts b/packages/api-rest/src/errors/validation.ts index 2f13fdb6831..ab454d06658 100644 --- a/packages/api-rest/src/errors/validation.ts +++ b/packages/api-rest/src/errors/validation.ts @@ -4,14 +4,10 @@ import { AmplifyErrorMap } from '@aws-amplify/core/internals/utils'; export enum RestApiValidationErrorCode { - NoCredentials = 'NoCredentials', InvalidApiName = 'InvalidApiName', } export const validationErrorMap: AmplifyErrorMap = { - [RestApiValidationErrorCode.NoCredentials]: { - message: 'Credentials should not be empty.', - }, [RestApiValidationErrorCode.InvalidApiName]: { message: 'API name is invalid.', recoverySuggestion: diff --git a/packages/api-rest/src/utils/index.ts b/packages/api-rest/src/utils/index.ts index 2e72b5bf24b..783308821f0 100644 --- a/packages/api-rest/src/utils/index.ts +++ b/packages/api-rest/src/utils/index.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 export { createCancellableOperation } from './createCancellableOperation'; -export { resolveCredentials } from './resolveCredentials'; export { parseSigningInfo } from './parseSigningInfo'; export { parseRestApiServiceError } from './serviceError'; export { resolveApiUrl } from './resolveApiUrl'; diff --git a/packages/api-rest/src/utils/resolveCredentials.ts b/packages/api-rest/src/utils/resolveCredentials.ts deleted file mode 100644 index 155606937aa..00000000000 --- a/packages/api-rest/src/utils/resolveCredentials.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { AmplifyClassV6 } from '@aws-amplify/core'; -import { RestApiValidationErrorCode, assertValidationError } from '../errors'; - -/** - * @internal - */ -export const resolveCredentials = async (amplify: AmplifyClassV6) => { - const { credentials } = await amplify.Auth.fetchAuthSession(); - assertValidationError( - !!credentials && !!credentials.accessKeyId && !!credentials.secretAccessKey, - RestApiValidationErrorCode.NoCredentials - ); - return credentials; -};