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
21 changes: 21 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,27 @@ describe('internal post', () => {
expect.objectContaining({ region: 'us-west-2', service: 'lambda' }),
);
});

it('should call authenticatedHandler for appsync-api service', async () => {
ashika112 marked this conversation as resolved.
Show resolved Hide resolved
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 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);
});
});
});
17 changes: 6 additions & 11 deletions packages/api-rest/src/apis/common/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,14 @@ 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
Expand All @@ -46,6 +41,10 @@ interface SigningServiceInfo {
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 +68,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 +97,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;
}
16 changes: 16 additions & 0 deletions packages/api-rest/src/utils/isIamAuthApplicable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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';

export const isIamAuthApplicableForGraphQL = (
{ headers }: HttpRequest,
signingServiceInfo?: SigningServiceInfo,
) => !headers.authorization && !headers['x-api-key'] && !!signingServiceInfo;

export const isIamAuthApplicableForRest = (
{ headers }: HttpRequest,
signingServiceInfo?: SigningServiceInfo,
) => !headers.authorization && !!signingServiceInfo;
Loading