Stack Creation Always Fails with Lambda Authorizers

I have defined several APIGateway endpoints in my microservice. As I am handling authentication with Auth0, I would have to create an authorizer which sends a request to Auth0’s authorization server to verify a user’s identity for each endpoint’s Lambda function.

While I am using LocalStack in my Docker container for testing deployments and running tests that send requests to the resources deployed to the container, I am working on bypassing the authorization step for local stage (for testing only) at this stage of development.

// serverless.yml
service: myapp-users
custom:
  localAuthorizer:
    type: token
    authorizerId:
      Ref: AlwaysAllowAuthorizer
  stageBasedJWTAuthorizer:
    type: jwt
    identitySource: method.request.header.Authorization
    issuerUrl: https://myapp-${self:provider.stage}.jp.auth0.com
    audience:
      - https://myapp-${self:provider.stage}-auth0-authorizer
  authorizers:
    local: ${self:custom.localAuthorizer}
    dev: ${self:custom.stageBasedJWTAuthorizer}
    staging: ${self:custom.stageBasedJWTAuthorizer}
    canary: ${self:custom.stageBasedJWTAuthorizer}
    prod:
      type: jwt
      identitySource: method.request.header.Authorization
      issuerUrl: https://myapp.jp.auth0.com
      audience:
        - https://myapp-auth0-authorizer
  accountIds:
    local: "000000000000" # Example AWS Account ID for local development
    default: ${AWS::AccountId} # Use the AWS pseudo parameter for normal deployments
  accountId: ${self:custom.accountIds.${self:provider.stage}, self:custom.accountIds.default}
  localstack:
    host: http://localhost
    edgePort: 4566
    stages:
      - local
    autostart: true
provider:
  name: aws
  endpointType: regional
  runtime: nodejs20.x
  region: ${opt:region, 'ap-east-1'} # Default to ap-east-1 if not specified
  stage: ${opt:stage, 'dev'} # Default to dev if not specified
  environment:
    REGION: ${self:provider.region}
    DYNAMODB_TABLE_NAME: myapp-users-${self:provider.stage}
  deploymentBucket:
    name: ${self:service}-${self:provider.stage}-deployments-${self:provider.region}
  # logs:
  #   restApi: true
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
          Resource:
            - !Sub arn:aws:dynamodb:${self:provider.region}:${self:custom.accountId}:table/${self:provider.environment.DYNAMODB_TABLE_NAME}
            - !Sub arn:aws:logs:${self:provider.region}:${self:custom.accountId}:log-group:/aws/lambda/${self:service}-*:*:*
plugins:
  - serverless-localstack
functions:
  alwaysAllowFunction:
    handler: dist/tests/alwaysAllow.handler
  createUser:
    handler: dist/createUser.handler
    events:
      - http:
          path: user/create
          method: post
          authorizer: ${self:custom.authorizers.${self:provider.stage}}
  updateUser:
    handler: dist/updateUser.handler
    events:
      - http:
          path: user/{userId}/update
          method: patch
          authorizer: ${self:custom.authorizers.${self:provider.stage}}
  deleteUser:
    handler: dist/deleteUser.handler
    events:
      - http:
          path: user/{userId}/delete
          method: delete
          authorizer: ${self:custom.authorizers.${self:provider.stage}}
  getUser:
    handler: dist/getUser.handler
    events:
      - http:
          path: user
          method: get
          authorizer: ${self:custom.authorizers.${self:provider.stage}}
resources:
  Conditions:
    IsLocal: !Equals ["${self:provider.stage}", "local"]
  Resources:
    AlwaysAllowAuthorizer:
      Type: "AWS::ApiGateway::Authorizer"
      DependsOn: MyappUsersRestApi
      Properties:
        Name: "always-allow-authorizer"
        Type: TOKEN
        RestApiId:
          Ref: MyappUsersRestApi
        IdentitySource: "method.request.header.Authorization"
        AuthorizerResultTtlInSeconds: 0
        AuthorizerUri:
          Fn::Join:
            - ''
            - - 'arn:aws:apigateway:'
              - Ref: 'AWS::Region'
              - ':lambda:path/2015-03-31/functions/arn:aws:lambda:'
              - Ref: 'AWS::Region'
              - ':'
              - Ref: 'AWS::AccountId'
              - ':function:alwaysAllowFunction/invocations'
        IdentityValidationExpression: null
    MyappUsersRestApi:
      Type: AWS::ApiGateway::RestApi
      Properties:
        Name: ${self:service}-${self:provider.stage}
    MyappUsersTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        TableName: myapp-users-${self:provider.stage}
        AttributeDefinitions:
                ......

// src/alwaysAllow.ts
import { APIGatewayTokenAuthorizerHandler } from "aws-lambda";

export const handler: APIGatewayTokenAuthorizerHandler = async (event) => {
  return {
    principalId: 'user',
    policyDocument: {
      Version: '2012-10-17',
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: 'Allow',
        Resource: event.methodArn
      }]
    }
  };
};

When I run serverless deploy --stage local --region ap-east-1 --verbose after yarn build and aws --endpoint-url=http://localhost:4566 s3 mb s3://..., the deployment is always stuck at the last moment where the stack creation fails:

Using serverless-localstack
Using serverless-localstack
serverless-localstack: Reconfigured endpoints
serverless-localstack: Reconfigured endpoints
Packaging
Excluding development dependencies for service package
Retrieving CloudFormation stack
Uploading
Uploading CloudFormation file to S3
Uploading State file to S3
Uploading service myapp-users.zip file to S3 (46.66 MB)
Skipping template validation: Unsupported in Localstack
Updating CloudFormation stack
Creating CloudFormation stack
Creating new change set
Waiting for new change set to be created
Executing created change set
  CREATE_IN_PROGRESS - AWS::CloudFormation::Stack - myapp-users-local
  CREATE_COMPLETE - AWS::Logs::LogGroup - AlwaysAllowFunctionLogGroup
  CREATE_COMPLETE - AWS::Logs::LogGroup - CreateUserLogGroup
  CREATE_COMPLETE - AWS::Logs::LogGroup - UpdateUserLogGroup
  CREATE_COMPLETE - AWS::Logs::LogGroup - DeleteUserLogGroup
  CREATE_COMPLETE - AWS::Logs::LogGroup - GetUserLogGroup
  CREATE_COMPLETE - AWS::IAM::Role - IamRoleLambdaExecution
  CREATE_COMPLETE - AWS::Lambda::Function - AlwaysAllowFunctionLambdaFunction
  CREATE_COMPLETE - AWS::Lambda::Function - CreateUserLambdaFunction
  CREATE_COMPLETE - AWS::Lambda::Function - UpdateUserLambdaFunction
  CREATE_COMPLETE - AWS::Lambda::Function - DeleteUserLambdaFunction
  CREATE_COMPLETE - AWS::Lambda::Function - GetUserLambdaFunction
  CREATE_COMPLETE - AWS::Lambda::Version - AlwaysAllowFunctionLambdaVersionlbjwY5JAl1rBFpOEefd4Yclq3qqG2MAXzztIUgRa0
  CREATE_COMPLETE - AWS::Lambda::Version - CreateUserLambdaVersionlMsoj4VDXZQHhFpgnPWfoRxHcQK6uAYE9UZsxpgUo
  CREATE_COMPLETE - AWS::Lambda::Version - UpdateUserLambdaVersion0cdpf2VjTkP70G1Ua2jQemFsj3XuyTn7QyJs0uGSb8
  CREATE_COMPLETE - AWS::Lambda::Version - DeleteUserLambdaVersiontkjw9aHaRwIKeyznYwwlE95tl5UrwvleLVw2vwAmjWs
  CREATE_COMPLETE - AWS::Lambda::Version - GetUserLambdaVersionF1dbTgow5XudYkS2wIsf72O4fN2fNIKeIZFWBlFM
  CREATE_COMPLETE - AWS::ApiGateway::RestApi - ApiGatewayRestApi
  CREATE_COMPLETE - AWS::ApiGateway::Resource - ApiGatewayResourceUser
  CREATE_COMPLETE - AWS::ApiGateway::Resource - ApiGatewayResourceUserCreate
  CREATE_COMPLETE - AWS::ApiGateway::Resource - ApiGatewayResourceUserUseridVar
  CREATE_COMPLETE - AWS::ApiGateway::Resource - ApiGatewayResourceUserUseridVarUpdate
  CREATE_COMPLETE - AWS::ApiGateway::Resource - ApiGatewayResourceUserUseridVarDelete
  CREATE_COMPLETE - AWS::ApiGateway::Resource - ApiGatewayResourceUserUseridVarLicensenumber
  CREATE_COMPLETE - AWS::ApiGateway::Resource - ApiGatewayResourceUserUseridVarLicensenumberLicensenumberVar
  CREATE_COMPLETE - AWS::Lambda::Permission - CreateUserLambdaPermissionApiGateway
  CREATE_COMPLETE - AWS::Lambda::Permission - UpdateUserLambdaPermissionApiGateway
  CREATE_COMPLETE - AWS::Lambda::Permission - DeleteUserLambdaPermissionApiGateway
  CREATE_COMPLETE - AWS::Lambda::Permission - GetUserLambdaPermissionApiGateway
  CREATE_COMPLETE - AWS::ApiGateway::RestApi - MyappUsersRestApi
  CREATE_COMPLETE - AWS::DynamoDB::Table - MyappUsersTable
  CREATE_COMPLETE - AWS::ApiGateway::Authorizer - AlwaysAllowAuthorizer
  CREATE_FAILED - AWS::CloudFormation::Stack - myapp-users-local
 Creating CloudFormation stack (36/42) (14630s)

I am not sure if I have configured incorrectly or there is incompatibility with the serverless-localstack plugin. Could anyone provide me some guidance on this issue? I am also open to test the authorizer instead of bypassing it.

Hi,
Thanks for reaching out. For better understanding your issue from a LocalStack standpoint could you please start LocalStack by setting the following env variables DEBUG=1 LS_LOG=trace and share the logs please?
Additionally can you clarify please your LocalStack license version, is it pro or the community edition?

Hello there. Thank you for replying!

I am using the community (free) version. Here is the error from the log:

 2024-04-30T11:01:35.639 DEBUG --- [functhread37] l.s.c.e.template_deployer  : Error applying changes for CloudFormation stack "myapp-users-local": Resource deployment loop completed, pending resource changes: [{'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserCreatePost', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserUseridVarUpdatePatch', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserUseridVarDeleteDelete', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserGet', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserUseridVarLicensenumberLicensenumberVarVerifyPatch', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayDeployment1714474849090', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Deployment', 'Scope': [], 'Details': [], '_deployed': False}}] Traceback (most recent call last):
2024-04-30 04:01:35 myapp-users  |   File "/opt/code/localstack/localstack/services/cloudformation/engine/template_deployer.py", line 1215, in _run
2024-04-30 04:01:35 myapp-users  |     self.do_apply_changes_in_loop(changes, stack)
2024-04-30 04:01:35 myapp-users  |   File "/opt/code/localstack/localstack/services/cloudformation/engine/template_deployer.py", line 1333, in do_apply_changes_in_loop
2024-04-30 04:01:35 myapp-users  |     raise Exception(
2024-04-30 04:01:35 myapp-users  | Exception: Resource deployment loop completed, pending resource changes: [{'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserCreatePost', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserUseridVarUpdatePatch', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserUseridVarDeleteDelete', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserGet', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayMethodUserUseridVarLicensenumberLicensenumberVarVerifyPatch', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Method', 'Scope': [], 'Details': []}}, {'Type': 'Resource', 'ResourceChange': {'Action': 'Add', 'LogicalResourceId': 'ApiGatewayDeployment1714474849090', 'PhysicalResourceId': None, 'ResourceType': 'AWS::ApiGateway::Deployment', 'Scope': [], 'Details': [], '_deployed': False}}]

Please let me know if you need anymore information. Thanks again!

Thanks for all the information.

So after consulting with my colleagues it seems that even though API Gateway v1 contains the CreateAuthorizer endpoint in the community version of LocalStack but this is only a mock.
To have proper emulation of the behaviour either with CFn or directly with API Gateway v2 unfortunately you need a Pro license key (docs).

Hope this helps, if you have any further questions, please do not hesitate to reach out again.

Thank you for clarifying this.

Given that the community version does not support directly with API Gateway v2 and the authorizer field does not accept accept "", null or undefined (otherwise it will return an error which says a field such as type, issuerUrl is missing for authorizer), I have set a couple of custom variables to correspond with each endpoint’s function so that the authorizer can be omitted by not being specified in the function definition. Here is my code in my serverless.yml for anyone who runs into this problem in the future:

custom:
  stageBasedJWTAuthorizer:
    type: jwt
    identitySource: method.request.header.Authorization
    issuerUrl: https://myapp-${self:provider.stage}.jp.auth0.com
    audience:
      - https://ddb-users-api-authorizer-for-auth0
  prodAuthorizer:
    type: jwt
    identitySource: method.request.header.Authorization
    issuerUrl: https://myapp.jp.auth0.com
    audience:
      - https://ddb-users-api-authorizer-for-auth0
  stageBasedFunction:
    createUser:
      handler: dist/createUser.handler
      events:
        - http:
            path: user/create
            method: post
            authorizer: ${self:custom.stageBasedJWTAuthorizer}
    getUser:
      handler: dist/getUser.handler
      events:
        - http:
            path: user
            method: get
            authorizer: ${self:custom.stageBasedJWTAuthorizer}
    updateUser:
      handler: dist/updateUser.handler
      events:
        - http:
            path: user/{userId}/update
            method: patch
            authorizer: ${self:custom.stageBasedJWTAuthorizer}
    deleteUser:
      handler: dist/deleteUser.handler
      events:
        - http:
            path: user/{userId}/delete
            method: delete
            authorizer: ${self:custom.stageBasedJWTAuthorizer}
  createUser:
    local:
      handler: dist/createUser.handler
      events:
        - http:
            path: user/create
            method: post
    dev: ${self:custom.stageBasedFunction.createUser}
    staging: ${self:custom.stageBasedFunction.createUser}
    canary: ${self:custom.stageBasedFunction.createUser}
    prod:
      handler: dist/createUser.handler
      events:
        - http:
            path: /user/create
            method: post
            authorizer: ${self:custom.prodAuthorizer}
  getUser:
    local:
      handler: dist/getUser.handler
      events:
        - http:
            path: user
            method: get
    dev: ${self:custom.stageBasedFunction.getUser}
    staging: ${self:custom.stageBasedFunction.getUser}
    canary: ${self:custom.stageBasedFunction.getUser}
    prod:
      handler: dist/getUser.handler
      events:
        - http:
            path: user
            method: get
            authorizer: ${self:custom.prodAuthorizer}
  updateUser:
    local:
      handler: dist/updateUser.handler
      events:
        - http:
            path: user/{userId}/update
            method: patch
    dev: ${self:custom.stageBasedFunction.updateUser}
    staging: ${self:custom.stageBasedFunction.updateUser}
    canary: ${self:custom.stageBasedFunction.updateUser}
    prod:
      handler: dist/updateUser.handler
      events:
        - http:
            path: user/{userId}/update
            method: patch
            authorizer: ${self:custom.prodAuthorizer}
  deleteUser:
    local:
      handler: dist/deleteUser.handler
      events:
        - http:
            path: user/{userId}/delete
            method: delete
    dev: ${self:custom.stageBasedFunction.deleteUser}
    staging: ${self:custom.stageBasedFunction.deleteUser}
    canary: ${self:custom.stageBasedFunction.deleteUser}
    prod:
      handler: dist/deleteUser.handler
      events:
        - http:
            path: user/{userId}/delete
            method: delete
            authorizer: ${self:custom.prodAuthorizer}
.....
functions:
  createUser: ${self:custom.createUser.${self:provider.stage}}
  updateUser: ${self:custom.updateUser.${self:provider.stage}}
  deleteUser: ${self:custom.deleteUser.${self:provider.stage}}
  getUser: ${self:custom.getUser.${self:provider.stage}}

Please let me know if the code could be further improved as well. Thank you again @lkkgr!