From 43aaa2e1960de15ce28748583919b0759879d147 Mon Sep 17 00:00:00 2001 From: frozenbonito Date: Wed, 20 Sep 2023 10:07:50 +0900 Subject: [PATCH] fix: support authorizer with no identity source specified (#1639) --- src/events/http/HttpServer.js | 14 +++- src/events/http/createAuthScheme.js | 70 ++++++++++++------- .../no-identity-source-authorizer.test.js | 54 ++++++++++++++ .../serverless.yml | 32 +++++++++ .../src/authorizer.js | 15 ++++ .../src/handler.js | 8 +++ .../src/package.json | 3 + 7 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 tests/integration/no-identity-source-authorizer/no-identity-source-authorizer.test.js create mode 100644 tests/integration/no-identity-source-authorizer/serverless.yml create mode 100644 tests/integration/no-identity-source-authorizer/src/authorizer.js create mode 100644 tests/integration/no-identity-source-authorizer/src/handler.js create mode 100644 tests/integration/no-identity-source-authorizer/src/package.json diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index 3f69628a1..3fc995289 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -323,9 +323,7 @@ export default class HttpServer { (endpoint.isHttpApi && serverlessAuthorizerOptions?.enableSimpleResponses) || false, - identitySource: - serverlessAuthorizerOptions?.identitySource || - 'method.request.header.Authorization', + identitySource: serverlessAuthorizerOptions?.identitySource, identityValidationExpression: serverlessAuthorizerOptions?.identityValidationExpression || '(.*)', payloadVersion: endpoint.isHttpApi @@ -351,6 +349,16 @@ export default class HttpServer { assign(authorizerOptions, endpoint.authorizer) } + if ( + !authorizerOptions.identitySource && + !( + authorizerOptions.type === 'request' && + authorizerOptions.resultTtlInSeconds === 0 + ) + ) { + authorizerOptions.identitySource = 'method.request.header.Authorization' + } + // Create a unique scheme per endpoint // This allows the methodArn on the event property to be set appropriately const authKey = `${functionKey}-${authFunctionName}-${method}-${path}` diff --git a/src/events/http/createAuthScheme.js b/src/events/http/createAuthScheme.js index 71b56a70f..ba49a589e 100644 --- a/src/events/http/createAuthScheme.js +++ b/src/events/http/createAuthScheme.js @@ -13,6 +13,7 @@ import { const IDENTITY_SOURCE_TYPE_HEADER = 'header' const IDENTITY_SOURCE_TYPE_QUERYSTRING = 'querystring' +const IDENTITY_SOURCE_TYPE_NONE = 'none' export default function createAuthScheme(authorizerOptions, provider, lambda) { const authFunName = authorizerOptions.name @@ -65,38 +66,50 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) { const methodArn = `arn:aws:execute-api:${provider.region}:${accountId}:${apiId}/${provider.stage}/${httpMethod}${resourcePath}` let authorization - if (identitySourceType === IDENTITY_SOURCE_TYPE_HEADER) { - const headers = request.raw.req.headers ?? {} - authorization = headers[identitySourceField] - } else if (identitySourceType === IDENTITY_SOURCE_TYPE_QUERYSTRING) { - const queryStringParameters = parseQueryStringParameters(url) ?? {} - authorization = queryStringParameters[identitySourceField] - } else { - throw new Error( - `No Authorization source has been specified. This should never happen. (λ: ${authFunName})`, - ) + switch (identitySourceType) { + case IDENTITY_SOURCE_TYPE_HEADER: { + const headers = request.raw.req.headers ?? {} + authorization = headers[identitySourceField] + break + } + case IDENTITY_SOURCE_TYPE_QUERYSTRING: { + const queryStringParameters = parseQueryStringParameters(url) ?? {} + authorization = queryStringParameters[identitySourceField] + break + } + case IDENTITY_SOURCE_TYPE_NONE: { + break + } + default: { + throw new Error( + `No Authorization source has been specified. This should never happen. (λ: ${authFunName})`, + ) + } } - if (authorization === undefined) { - log.error( - `Identity Source is null for ${identitySourceType} ${identitySourceField} (λ: ${authFunName})`, + let finalAuthorization + if (identitySourceType !== IDENTITY_SOURCE_TYPE_NONE) { + if (authorization === undefined) { + log.error( + `Identity Source is null for ${identitySourceType} ${identitySourceField} (λ: ${authFunName})`, + ) + return Boom.unauthorized( + 'User is not authorized to access this resource', + ) + } + + const identityValidationExpression = new RegExp( + authorizerOptions.identityValidationExpression, ) - return Boom.unauthorized( - 'User is not authorized to access this resource', + const matchedAuthorization = + identityValidationExpression.test(authorization) + finalAuthorization = matchedAuthorization ? authorization : '' + + log.debug( + `Retrieved ${identitySourceField} ${identitySourceType} "${finalAuthorization}"`, ) } - const identityValidationExpression = new RegExp( - authorizerOptions.identityValidationExpression, - ) - const matchedAuthorization = - identityValidationExpression.test(authorization) - const finalAuthorization = matchedAuthorization ? authorization : '' - - log.debug( - `Retrieved ${identitySourceField} ${identitySourceType} "${finalAuthorization}"`, - ) - if (authorizerOptions.payloadVersion === '1.0') { event = { ...event, @@ -296,5 +309,10 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) { ) } + if (authorizerOptions.resultTtlInSeconds === 0) { + identitySourceType = IDENTITY_SOURCE_TYPE_NONE + return finalizeAuthScheme() + } + return finalizeAuthScheme() } diff --git a/tests/integration/no-identity-source-authorizer/no-identity-source-authorizer.test.js b/tests/integration/no-identity-source-authorizer/no-identity-source-authorizer.test.js new file mode 100644 index 000000000..10982e08f --- /dev/null +++ b/tests/integration/no-identity-source-authorizer/no-identity-source-authorizer.test.js @@ -0,0 +1,54 @@ +import assert from 'node:assert' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { BASE_URL } from '../../config.js' +import { setup, teardown } from '../../_testHelpers/index.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +describe('no identity source authorizer tests', function desc() { + beforeEach(() => + setup({ + servicePath: resolve(__dirname), + }), + ) + + afterEach(() => teardown()) + + // + ;[ + { + description: 'should respond with 200', + expected: { + message: 'hello', + }, + options: { + headers: { + Authorization: 'Bearer 4674cc54-bd05-11e7-abc4-cec278b6b50a', + }, + }, + path: '/dev/hello', + status: 200, + }, + + { + description: + 'should respond with 200 if request has no authorization header', + expected: { + message: 'hello', + }, + path: '/dev/hello', + status: 200, + }, + ].forEach(({ description, expected, options, path, status }) => { + it(description, async () => { + const url = new URL(path, BASE_URL) + + const response = await fetch(url, options) + assert.equal(response.status, status) + + const json = await response.json() + assert.deepEqual(json, expected) + }) + }) +}) diff --git a/tests/integration/no-identity-source-authorizer/serverless.yml b/tests/integration/no-identity-source-authorizer/serverless.yml new file mode 100644 index 000000000..f914e544a --- /dev/null +++ b/tests/integration/no-identity-source-authorizer/serverless.yml @@ -0,0 +1,32 @@ +service: no-identity-source-authorizer + +configValidationMode: error +deprecationNotificationMode: error + +plugins: + - ../../../src/index.js + +provider: + architecture: arm64 + deploymentMethod: direct + memorySize: 1024 + name: aws + region: us-east-1 + runtime: nodejs18.x + stage: dev + versionFunctions: false + +functions: + hello: + events: + - http: + authorizer: + name: authorizer + resultTtlInSeconds: 0 + type: request + method: get + path: hello + handler: src/handler.hello + + authorizer: + handler: src/authorizer.authorizer diff --git a/tests/integration/no-identity-source-authorizer/src/authorizer.js b/tests/integration/no-identity-source-authorizer/src/authorizer.js new file mode 100644 index 000000000..e4fbc80de --- /dev/null +++ b/tests/integration/no-identity-source-authorizer/src/authorizer.js @@ -0,0 +1,15 @@ +export async function authorizer(event) { + return { + policyDocument: { + Statement: [ + { + Action: 'execute-api:Invoke', + Effect: 'Allow', + Resource: event.methodArn, + }, + ], + Version: '2012-10-17', + }, + principalId: 'user', + } +} diff --git a/tests/integration/no-identity-source-authorizer/src/handler.js b/tests/integration/no-identity-source-authorizer/src/handler.js new file mode 100644 index 000000000..865918d5f --- /dev/null +++ b/tests/integration/no-identity-source-authorizer/src/handler.js @@ -0,0 +1,8 @@ +const { stringify } = JSON + +export async function hello() { + return { + body: stringify({ message: 'hello' }), + statusCode: 200, + } +} diff --git a/tests/integration/no-identity-source-authorizer/src/package.json b/tests/integration/no-identity-source-authorizer/src/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/integration/no-identity-source-authorizer/src/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +}