From 13cf7441b524eaaaac314eee596b97062da36dbe Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Thu, 22 Aug 2024 11:23:35 -0700 Subject: [PATCH] Allow x-api-key for public rest api (#13422) * refactor iamAuthApplicable * cleanup & test * update to sample token * clean lint error * cleanup signingService type * update function maing * moving default to internalPost * rename file * update ts docs * add test with specified service in test suite --- .../apis/common/internalPost.test.ts | 41 ++++++++++ .../utils/isIamAuthApplicable.test.ts | 74 +++++++++++++++++++ packages/api-rest/src/apis/common/handler.ts | 18 ++--- .../api-rest/src/apis/common/internalPost.ts | 2 + .../api-rest/src/apis/common/publicApis.ts | 2 + packages/api-rest/src/types/index.ts | 9 +++ .../api-rest/src/utils/isIamAuthApplicable.ts | 44 +++++++++++ 7 files changed, 179 insertions(+), 11 deletions(-) create mode 100644 packages/api-rest/__tests__/utils/isIamAuthApplicable.test.ts create mode 100644 packages/api-rest/src/utils/isIamAuthApplicable.ts diff --git a/packages/api-rest/__tests__/apis/common/internalPost.test.ts b/packages/api-rest/__tests__/apis/common/internalPost.test.ts index acfd9ae4530..f4887e9d2ad 100644 --- a/packages/api-rest/__tests__/apis/common/internalPost.test.ts +++ b/packages/api-rest/__tests__/apis/common/internalPost.test.ts @@ -91,6 +91,47 @@ describe('internal post', () => { expect.objectContaining({ region: 'us-west-2', service: 'lambda' }), ); }); + + it('should call authenticatedHandler for appsync-api service with default signing name', async () => { + const appsyncApiEndpoint = new URL( + 'https://123.appsync-api.us-west-2.amazonaws.com/graphql', + ); + await post(mockAmplifyInstance, { + url: appsyncApiEndpoint, + options: { + signingServiceInfo: { region: 'us-east-1' }, + }, + }); + expect(mockAuthenticatedHandler).toHaveBeenCalledWith( + { + url: appsyncApiEndpoint, + method: 'POST', + headers: {}, + }, + expect.objectContaining({ region: 'us-east-1', service: 'appsync' }), + ); + }); + + it('should call authenticatedHandler for appsync-api with specified service from signingServiceInfo', async () => { + const appsyncApiEndpoint = new URL( + 'https://123.appsync-api.us-west-2.amazonaws.com/graphql', + ); + await post(mockAmplifyInstance, { + url: appsyncApiEndpoint, + options: { + signingServiceInfo: { service: 'appsync', region: 'us-east-1' }, + }, + }); + expect(mockAuthenticatedHandler).toHaveBeenCalledWith( + { + url: appsyncApiEndpoint, + method: 'POST', + headers: {}, + }, + expect.objectContaining({ region: 'us-east-1', service: 'appsync' }), + ); + }); + it('should call authenticatedHandler with empty signingServiceInfo', async () => { await post(mockAmplifyInstance, { url: apiGatewayUrl, diff --git a/packages/api-rest/__tests__/utils/isIamAuthApplicable.test.ts b/packages/api-rest/__tests__/utils/isIamAuthApplicable.test.ts new file mode 100644 index 00000000000..a75733f738a --- /dev/null +++ b/packages/api-rest/__tests__/utils/isIamAuthApplicable.test.ts @@ -0,0 +1,74 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { HttpRequest } from '@aws-amplify/core/internals/aws-client-utils'; + +import { + isIamAuthApplicableForGraphQL, + isIamAuthApplicableForRest, +} from '../../src/utils/isIamAuthApplicable'; + +describe('iamAuthApplicable', () => { + const url = new URL('https://url'); + const baseRequest: HttpRequest = { + headers: {}, + url, + method: 'put', + }; + + describe('iamAuthApplicableForGraphQL', () => { + it('should return true if there is no authorization header, no x-api-key header, and signingServiceInfo is provided', () => { + const signingServiceInfo = {}; + expect( + isIamAuthApplicableForGraphQL(baseRequest, signingServiceInfo), + ).toBe(true); + }); + + it('should return false if there is an authorization header', () => { + const request = { + ...baseRequest, + headers: { authorization: 'SampleToken' }, + }; + const signingServiceInfo = {}; + expect(isIamAuthApplicableForGraphQL(request, signingServiceInfo)).toBe( + false, + ); + }); + + it('should return false if there is an x-api-key header', () => { + const request = { ...baseRequest, headers: { 'x-api-key': 'key' } }; + const signingServiceInfo = {}; + expect(isIamAuthApplicableForGraphQL(request, signingServiceInfo)).toBe( + false, + ); + }); + + it('should return false if signingServiceInfo is not provided', () => { + expect(isIamAuthApplicableForGraphQL(baseRequest)).toBe(false); + }); + }); + + describe('iamAuthApplicableForPublic', () => { + it('should return true if there is no authorization header and signingServiceInfo is provided', () => { + const signingServiceInfo = {}; + expect(isIamAuthApplicableForRest(baseRequest, signingServiceInfo)).toBe( + true, + ); + }); + + it('should return false if there is an authorization header', () => { + const request = { + ...baseRequest, + headers: { authorization: 'SampleToken' }, + }; + const signingServiceInfo = {}; + expect(isIamAuthApplicableForRest(request, signingServiceInfo)).toBe( + false, + ); + }); + + it('should return false if signingServiceInfo is not provided', () => { + expect(isIamAuthApplicableForRest(baseRequest)).toBe(false); + }); + }); +}); diff --git a/packages/api-rest/src/apis/common/handler.ts b/packages/api-rest/src/apis/common/handler.ts index 9ae021e4e4e..d17a8c72b6a 100644 --- a/packages/api-rest/src/apis/common/handler.ts +++ b/packages/api-rest/src/apis/common/handler.ts @@ -20,7 +20,7 @@ import { parseSigningInfo, } from '../../utils'; import { resolveHeaders } from '../../utils/resolveHeaders'; -import { RestApiResponse } from '../../types'; +import { RestApiResponse, SigningServiceInfo } from '../../types'; type HandlerOptions = Omit & { body?: DocumentType | FormData; @@ -28,11 +28,6 @@ type HandlerOptions = Omit & { withCredentials?: boolean; }; -interface SigningServiceInfo { - service?: string; - region?: string; -} - /** * Make REST API call with best-effort IAM auth. * @param amplify Amplify instance to to resolve credentials and tokens. Should use different instance in client-side @@ -40,12 +35,17 @@ interface SigningServiceInfo { * @param options Options accepted from public API options when calling the handlers. * @param signingServiceInfo Internal-only options enable IAM auth as well as to to overwrite the IAM signing service * and region. If specified, and NONE of API Key header or Auth header is present, IAM auth will be used. + * @param iamAuthApplicable Callback function that is used to determine if IAM Auth should be used or not. * * @internal */ export const transferHandler = async ( amplify: AmplifyClassV6, options: HandlerOptions & { abortSignal: AbortSignal }, + iamAuthApplicable: ( + { headers }: HttpRequest, + signingServiceInfo?: SigningServiceInfo, + ) => boolean, signingServiceInfo?: SigningServiceInfo, ): Promise => { const { url, method, headers, body, withCredentials, abortSignal } = options; @@ -69,6 +69,7 @@ export const transferHandler = async ( }; const isIamAuthApplicable = iamAuthApplicable(request, signingServiceInfo); + let response: RestApiResponse; const credentials = await resolveCredentials(amplify); if (isIamAuthApplicable && credentials) { @@ -97,11 +98,6 @@ export const transferHandler = async ( }; }; -const iamAuthApplicable = ( - { headers }: HttpRequest, - signingServiceInfo?: SigningServiceInfo, -) => !headers.authorization && !headers['x-api-key'] && !!signingServiceInfo; - const resolveCredentials = async ( amplify: AmplifyClassV6, ): Promise => { diff --git a/packages/api-rest/src/apis/common/internalPost.ts b/packages/api-rest/src/apis/common/internalPost.ts index 574e5eb3da8..6dabea22072 100644 --- a/packages/api-rest/src/apis/common/internalPost.ts +++ b/packages/api-rest/src/apis/common/internalPost.ts @@ -6,6 +6,7 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { InternalPostInput, RestApiResponse } from '../../types'; import { createCancellableOperation } from '../../utils'; import { CanceledError } from '../../errors'; +import { isIamAuthApplicableForGraphQL } from '../../utils/isIamAuthApplicable'; import { transferHandler } from './handler'; @@ -66,6 +67,7 @@ export const post = ( ...options, abortSignal: controller.signal, }, + isIamAuthApplicableForGraphQL, options?.signingServiceInfo, ); diff --git a/packages/api-rest/src/apis/common/publicApis.ts b/packages/api-rest/src/apis/common/publicApis.ts index 6a132a6b277..8c7a58cb6fc 100644 --- a/packages/api-rest/src/apis/common/publicApis.ts +++ b/packages/api-rest/src/apis/common/publicApis.ts @@ -25,6 +25,7 @@ import { parseSigningInfo, resolveApiUrl, } from '../../utils'; +import { isIamAuthApplicableForRest } from '../../utils/isIamAuthApplicable'; import { transferHandler } from './handler'; @@ -71,6 +72,7 @@ const publicHandler = ( headers, abortSignal, }, + isIamAuthApplicableForRest, signingServiceInfo, ); }); diff --git a/packages/api-rest/src/types/index.ts b/packages/api-rest/src/types/index.ts index f2f3c214b17..7e0ecb61e7c 100644 --- a/packages/api-rest/src/types/index.ts +++ b/packages/api-rest/src/types/index.ts @@ -112,3 +112,12 @@ export interface InternalPostInput { */ abortController?: AbortController; } + +/** + * Type for signingServiceInfo which enable IAM auth as well as overwrite the IAM signing info. + * @internal + */ +export interface SigningServiceInfo { + service?: string; + region?: string; +} diff --git a/packages/api-rest/src/utils/isIamAuthApplicable.ts b/packages/api-rest/src/utils/isIamAuthApplicable.ts new file mode 100644 index 00000000000..ba48e7be3de --- /dev/null +++ b/packages/api-rest/src/utils/isIamAuthApplicable.ts @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { HttpRequest } from '@aws-amplify/core/internals/aws-client-utils'; + +import { SigningServiceInfo } from '../types'; + +/** + * Determines if IAM authentication should be applied for a GraphQL request. + * + * This function checks the `headers` of the HTTP request to determine if IAM authentication + * is applicable. IAM authentication is considered applicable if there is no `authorization` + * header, no `x-api-key` header, and `signingServiceInfo` is provided. + * + * @param request - The HTTP request object containing headers. + * @param signingServiceInfo - Optional signing service information, + * including service and region. + * @returns A boolean `true` if IAM authentication should be applied. + * + * @internal + */ +export const isIamAuthApplicableForGraphQL = ( + { headers }: HttpRequest, + signingServiceInfo?: SigningServiceInfo, +) => !headers.authorization && !headers['x-api-key'] && !!signingServiceInfo; + +/** + * Determines if IAM authentication should be applied for a REST request. + * + * This function checks the `headers` of the HTTP request to determine if IAM authentication + * is applicable. IAM authentication is considered applicable if there is no `authorization` + * header and `signingServiceInfo` is provided. + * + * @param request - The HTTP request object containing headers. + * @param signingServiceInfo - Optional signing service information, + * including service and region. + * @returns A boolean `true` if IAM authentication should be applied. + * + * @internal + */ +export const isIamAuthApplicableForRest = ( + { headers }: HttpRequest, + signingServiceInfo?: SigningServiceInfo, +) => !headers.authorization && !!signingServiceInfo;