Skip to content

Commit

Permalink
Allow x-api-key for public rest api (#13422)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ashika112 authored Aug 22, 2024
1 parent 5647497 commit 13cf744
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 11 deletions.
41 changes: 41 additions & 0 deletions packages/api-rest/__tests__/apis/common/internalPost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
74 changes: 74 additions & 0 deletions packages/api-rest/__tests__/utils/isIamAuthApplicable.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
18 changes: 7 additions & 11 deletions packages/api-rest/src/apis/common/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,32 @@ import {
parseSigningInfo,
} from '../../utils';
import { resolveHeaders } from '../../utils/resolveHeaders';
import { RestApiResponse } from '../../types';
import { RestApiResponse, SigningServiceInfo } from '../../types';

type HandlerOptions = Omit<HttpRequest, 'body' | 'headers'> & {
body?: DocumentType | FormData;
headers?: Headers;
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
* and SSR
* @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<RestApiResponse> => {
const { url, method, headers, body, withCredentials, abortSignal } = options;
Expand All @@ -69,6 +69,7 @@ export const transferHandler = async (
};

const isIamAuthApplicable = iamAuthApplicable(request, signingServiceInfo);

let response: RestApiResponse;
const credentials = await resolveCredentials(amplify);
if (isIamAuthApplicable && credentials) {
Expand Down Expand Up @@ -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<AWSCredentials | null> => {
Expand Down
2 changes: 2 additions & 0 deletions packages/api-rest/src/apis/common/internalPost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -66,6 +67,7 @@ export const post = (
...options,
abortSignal: controller.signal,
},
isIamAuthApplicableForGraphQL,
options?.signingServiceInfo,
);

Expand Down
2 changes: 2 additions & 0 deletions packages/api-rest/src/apis/common/publicApis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
parseSigningInfo,
resolveApiUrl,
} from '../../utils';
import { isIamAuthApplicableForRest } from '../../utils/isIamAuthApplicable';

import { transferHandler } from './handler';

Expand Down Expand Up @@ -71,6 +72,7 @@ const publicHandler = (
headers,
abortSignal,
},
isIamAuthApplicableForRest,
signingServiceInfo,
);
});
Expand Down
9 changes: 9 additions & 0 deletions packages/api-rest/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
44 changes: 44 additions & 0 deletions packages/api-rest/src/utils/isIamAuthApplicable.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 13cf744

Please sign in to comment.