Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow x-api-key for public rest api #13422

Merged
merged 13 commits into from
Aug 22, 2024
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: (
ashika112 marked this conversation as resolved.
Show resolved Hide resolved
{ headers }: HttpRequest,
signingServiceInfo?: SigningServiceInfo,
) => boolean,
signingServiceInfo?: SigningServiceInfo,
): Promise<RestApiResponse> => {
svidgen marked this conversation as resolved.
Show resolved Hide resolved
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 { 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 @@ -46,7 +47,7 @@
* @param postInput.abortController The abort controller used to cancel the POST request
* @returns a {@link RestApiResponse}
*
* @throws an {@link AmplifyError} with `Network Error` as the `message` when the external resource is unreachable due to one

Check warning on line 50 in packages/api-rest/src/apis/common/internalPost.ts

View workflow job for this annotation

GitHub Actions / unit-tests / Unit Test - @aws-amplify/api-rest

The type 'AmplifyError' is undefined
* of the following reasons:
* 1. no network connection
* 2. CORS error
Expand All @@ -66,6 +67,7 @@
...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;
Loading