UNPKG

serverless-offline

Version:

Emulate AWS λ and API Gateway locally when developing your Serverless project

317 lines (277 loc) 10.5 kB
import Boom from "@hapi/boom" import { log } from "../../utils/log.js" import authCanExecuteResource from "../authCanExecuteResource.js" import authValidateContext from "../authValidateContext.js" import { getRawQueryParams, nullIfEmpty, parseHeaders, parseMultiValueHeaders, parseMultiValueQueryStringParameters, parseQueryStringParameters, } from "../../utils/index.js" 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 let identitySourceField = "authorization" let identitySourceType = IDENTITY_SOURCE_TYPE_HEADER const finalizeAuthScheme = () => { return () => ({ async authenticate(request, h) { log.notice() log.notice( `Running Authorization function for ${request.method} ${request.path} (λ: ${authFunName})`, ) const { rawHeaders, url } = request.raw.req // Get path params // aws doesn't auto decode path params - hapi does const pathParams = { ...request.params } const accountId = "random-account-id" const apiId = "random-api-id" const requestId = "random-request-id" const httpMethod = request.method.toUpperCase() const resourcePath = request.route.path.replace( new RegExp(`^/${provider.stage}`), "", ) let event = { enhancedAuthContext: {}, headers: parseHeaders(rawHeaders), requestContext: { accountId, apiId, domainName: `${apiId}.execute-api.us-east-1.amazonaws.com`, domainPrefix: apiId, requestId, stage: provider.stage, }, version: authorizerOptions.payloadVersion, } const protocol = `${request.server.info.protocol.toUpperCase()}/${ request.raw.req.httpVersion }` const currentDate = new Date() const resourceId = `${httpMethod} ${resourcePath}` const methodArn = `arn:aws:execute-api:${provider.region}:${accountId}:${apiId}/${provider.stage}/${httpMethod}${resourcePath}` let authorization 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})`, ) } } 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, ) const matchedAuthorization = identityValidationExpression.test(authorization) finalAuthorization = matchedAuthorization ? authorization : "" log.debug( `Retrieved ${identitySourceField} ${identitySourceType} "${finalAuthorization}"`, ) } if (authorizerOptions.payloadVersion === "1.0") { event = { ...event, authorizationToken: finalAuthorization, httpMethod: request.method.toUpperCase(), identitySource: finalAuthorization, methodArn, multiValueHeaders: parseMultiValueHeaders(rawHeaders), multiValueQueryStringParameters: parseMultiValueQueryStringParameters(url), path: request.path, pathParameters: nullIfEmpty(pathParams), queryStringParameters: parseQueryStringParameters(url), requestContext: { ...event.requestContext, extendedRequestId: requestId, httpMethod, path: request.path, protocol, requestTime: currentDate.toString(), requestTimeEpoch: currentDate.getTime(), resourceId, resourcePath, stage: provider.stage, }, resource: resourcePath, } } if (authorizerOptions.payloadVersion === "2.0") { event = { ...event, identitySource: [finalAuthorization], pathParameters: nullIfEmpty(pathParams), queryStringParameters: parseQueryStringParameters(url), rawPath: request.path, rawQueryString: getRawQueryParams(url), requestContext: { ...event.requestContext, http: { method: httpMethod, path: resourcePath, protocol, }, routeKey: resourceId, time: currentDate.toString(), timeEpoch: currentDate.getTime(), }, routeArn: methodArn, routeKey: resourceId, } } // methodArn is the ARN of the function we are running we are authorizing access to (or not) // Account ID and API ID are not simulated event = { ...event, // This is safe since type: 'TOKEN' cannot have payload format 2.0 type: authorizerOptions.type === "request" ? "REQUEST" : "TOKEN", } const lambdaFunction = lambda.get(authFunName) lambdaFunction.setEvent(event) try { const result = await lambdaFunction.runHandler() if (authorizerOptions.enableSimpleResponses) { if (result.isAuthorized) { const authorizer = { integrationLatency: "42", ...result.context, } return h.authenticated({ credentials: { authorizer, context: result.context || {}, }, }) } return Boom.forbidden( "User is not authorized to access this resource", ) } if (result === "Unauthorized") return Boom.unauthorized("Unauthorized") // Validate that the policy document has the principalId set if (!result.principalId) { log.notice( `Authorization response did not include a principalId: (λ: ${authFunName})`, ) return Boom.forbidden("No principalId set on the Response") } if ( !authCanExecuteResource( result.policyDocument, event.methodArn || event.routeArn, ) ) { log.notice( `Authorization response didn't authorize user to access resource: (λ: ${authFunName})`, ) return Boom.forbidden( "User is not authorized to access this resource", ) } // validate the resulting context, ensuring that all // values are either string, number, or boolean types if (result.context) { const validationResult = authValidateContext( result.context, authFunName, ) if (validationResult instanceof Error) { return validationResult } result.context = validationResult } log.notice( `Authorization function returned a successful response: (λ: ${authFunName})`, ) const authorizer = { integrationLatency: "42", principalId: result.principalId, ...result.context, } // Set the credentials for the rest of the pipeline return h.authenticated({ credentials: { authorizer, context: result.context, principalId: result.principalId, usageIdentifierKey: result.usageIdentifierKey, }, }) } catch { log.notice( `Authorization function returned an error response: (λ: ${authFunName})`, ) return Boom.unauthorized("Unauthorized") } }, }) } const checkForIdentitySourceMatch = (exp, expectedLength) => { const identitySourceMatch = exp.exec(authorizerOptions.identitySource) if (!identitySourceMatch || identitySourceMatch.length !== expectedLength) { return undefined } return identitySourceMatch[expectedLength - 1] } if ( authorizerOptions.type !== "request" || authorizerOptions.identitySource ) { // Only validate the first of N possible headers. const headerRegExp = /^(method.|\$)request.header.((?:\w+-?)+\w+).*$/ const queryStringRegExp = /^(method.|\$)request.querystring.((?:\w+-?)+\w+)$/ const identityHeaderResult = checkForIdentitySourceMatch(headerRegExp, 3) if (identityHeaderResult !== undefined) { identitySourceField = identityHeaderResult.toLowerCase() identitySourceType = IDENTITY_SOURCE_TYPE_HEADER return finalizeAuthScheme() } const identityQueryStringResult = checkForIdentitySourceMatch( queryStringRegExp, 3, ) if (identityQueryStringResult !== undefined) { identitySourceField = identityQueryStringResult identitySourceType = IDENTITY_SOURCE_TYPE_QUERYSTRING return finalizeAuthScheme() } throw new Error( `Serverless Offline only supports retrieving tokens from headers and querystring parameters (λ: ${authFunName})`, ) } if (authorizerOptions.resultTtlInSeconds === 0) { identitySourceType = IDENTITY_SOURCE_TYPE_NONE return finalizeAuthScheme() } return finalizeAuthScheme() }