Shared Lambda authorizer setup in Serverless Frame

2020-07-11 04:46发布

问题:

I am trying to create a custom Lambda authorizer that will be shared between a few different services/serverless stacks. If I understand the documentation here https://serverless.com/framework/docs/providers/aws/events/apigateway/#note-while-using-authorizers-with-shared-api-gateway, that means that I need to create a shared authorizer resource in a “common resources” service/serverless stack, and then refer to that shared authorizer from my other services. First of all: Is my understanding correct?

If my understanding is correct, my next question becomes: How do I do this? The documentation doesn’t provide a clear example for lambda authorizers, so here’s how I tried to customize it:

functions:
authorizerFunc:
handler: authorizer/authorizer.handler
runtime: nodejs8.10

resources:
Resources:
authorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
AuthorizerResultTtlInSeconds: 0
Name: Authorizer
Type: REQUEST
AuthorizerUri: ???
RestApiId:
Fn::ImportValue: myRestApiId

I don’t understand what the syntax for AuthorizerUri is supposed to be. I’ve tried “Ref: authorizerFunc”, “Fn::GetAtt: [authorizerFunc, Arn]” etc. to no avail.

When I get the authorizerUri working, do I just add an Output for my authorizer resource, then Fn::ImportValue it from the services containing my API Lambdas?

Link to my question on the Serverless forum for posterity: https://forum.serverless.com/t/shared-lambda-authorizer/6447

回答1:

EDIT: Apparently my answer is now outdated. For newer versions of servicestack, see the other answers. I don't know which answer is best/most up-to-date, but if someone lets me know I'll change which answer is accepted to that one.

I eventually got it to work, so here's how I set up my autherizer's serverless.yml:

service: user-admin-authorizer

custom:
  region: ${file(serverless.env.yml):${opt:stage}.REGION}

provider:
  name: aws
  region: ${self:custom.region}

functions:
  authorizer:
    handler: src/authorizer.handler
    runtime: nodejs8.10

resources:
  Resources:
    Authorizer:
      Type: AWS::ApiGateway::Authorizer
      Properties:
        Name: Authorizer
        Type: REQUEST
        AuthorizerUri:
          Fn::Join: [ "",
            [
              "arn:aws:apigateway:",
              "${self:custom.region}",
              ":lambda:path/",
              "2015-03-31/functions/",
              Fn::GetAtt: ["AuthorizerLambdaFunction", "Arn" ],
              "/invocations"
            ]]
        RestApiId:
          Fn::ImportValue: api-gateway:${opt:stage}:rest-api-id
    apiGatewayLambdaPermissions:
      Type: AWS::Lambda::Permission
      Properties:
        FunctionName:
          Fn::GetAtt: [ AuthorizerLambdaFunction, Arn]
        Action: lambda:InvokeFunction
        Principal:
          Fn::Join: [ "",
          [
            "apigateway.",
            Ref: AWS::URLSuffix
          ]]

  Outputs:
    AuthorizerRef:
      Value:
        Ref: Authorizer
      Export:
        Name: authorizer-ref:${opt:stage}

Things to note: Even though the authorizer function is called "authorizer", you need to capitalize the first letter and append "LambdaFunction" to its name when using it with GetAtt, so "authorizer" becomes "AuthorizerLambdaFunction" for some reason. I also had to add the lambda permission resource.

The API gateway resource also needs two outputs, its API ID and its API root resource ID. Here's how my API gateway's serverless.yml is set up:

resources:
  Resources:
    ApiGateway:
      Type: AWS::ApiGateway::RestApi
      Properties:
        Name: ApiGateway

  Outputs:
    ApiGatewayRestApiId:
      Value:
        Ref: ApiGateway
      Export:
        Name: api-gateway:${opt:stage}:rest-api-id
    ApiGatewayRestApiRootResourceId:
      Value:
        Fn::GetAtt:
          - ApiGateway
          - RootResourceId
      Export:
        Name: api-gateway:${opt:stage}:root-resource-id

Now you just need to specify to your other services that they should use this API gateway (the imported values are the outputs of the API gateway):

provider:
  name: aws
  apiGateway:
    restApiId:
      Fn::ImportValue: api-gateway:${opt:stage}:rest-api-id
    restApiRootResourceId:
      Fn::ImportValue: api-gateway:${opt:stage}:root-resource-id

After that, the authorizer can be added to individual functions in this service like so:

          authorizer:
            type: CUSTOM
            authorizerId:
              Fn::ImportValue: authorizer-ref:${opt:stage}


回答2:

I had the same issue that you describe. Or at least I think so. And I managed to get it solved by following the documentation on links you provided.

The serverless documentation states for the authorizer format to be

authorizer:
  # Provide both type and authorizerId
  type: COGNITO_USER_POOLS # TOKEN or COGNITO_USER_POOLS, same as AWS Cloudformation documentation
  authorizerId: 
    Ref: ApiGatewayAuthorizer  # or hard-code Authorizer ID

Per my understanding, my solution (provide below) follows the hard-coded authorizer ID approach.

In the service that has the shared authorizer, it is declared in the serverless.yml in normal fashion, i.e.

functions:
  myCustomAuthorizer:
    handler: path/to/authorizer.handler
    name: my-shared-custom-authorizer

Then in the service that wishes to use this shared authorizer, the function in servlerless.yml is declared as

functions:
  foo:
    # some properties ...
    events:
      - http:
          # ... other properties ...
          authorizer:
            name: authorize
            arn:
              Fn::Join:
                - ""
                - - "arn:aws:lambda"
                  # References to values such as region, account id, stage, etc
                  # Can be done with Pseudo Parameter Reference
                  - ":"
                  - "function:myCustomAuthorizer"

It was crucial to add the name property. It would not work without it, at least at the moment.

For details see

  • ARN naming conventions
  • Pseudo Parameter Reference
  • Fn::Join

Unfortunately I cannot say whether this approach has some limitations compared to your suggestion of defining authorizer as a resource. In fact, that might make it easier to re-use the same authorizer in multiple functions within same service.



回答3:

Serverless 1.35.1 For people stumbling across this thread, here is the new way

Wherever you create the user pool, you can go ahead and add ApiGatewayAuthorizer

# create a user pool as normal
CognitoUserPoolClient:
  Type: AWS::Cognito::UserPoolClient
  Properties:
    # Generate an app client name based on the stage
    ClientName: ${self:custom.stage}-user-pool-client
    UserPoolId:
      Ref: CognitoUserPool
   ExplicitAuthFlows:
   - ADMIN_NO_SRP_AUTH
   GenerateSecret: true

# then add an authorizer you can reference later
ApiGatewayAuthorizer:
  DependsOn:
  # this is pre-defined by serverless
  - ApiGatewayRestApi
  Type: AWS::ApiGateway::Authorizer
  Properties:
    Name: cognito_auth
    # apparently ApiGatewayRestApi is a global string
    RestApiId: { "Ref" : "ApiGatewayRestApi" }
    IdentitySource: method.request.header.Authorization
    Type: COGNITO_USER_POOLS
    ProviderARNs:
    - Fn::GetAtt: [CognitoUserPool, Arn]

Then when you define your functions

graphql:
  handler: src/app.graphqlHandler
  events:
  - http:
    path: /
    method: post
    cors: true
    integration: lambda
    # add this and just reference the authorizer
    authorizer:
      type: COGNITO_USER_POOLS
      authorizerId:
        Ref: ApiGatewayAuthorizer


回答4:

This is how I did my set up since the answer posted above didn't worked for me. May be it could be helpful for someone.

resources:
  Resources:
    ApiGatewayAuthorizer:
      Type: AWS::ApiGateway::Authorizer
      Properties:
        AuthorizerResultTtlInSeconds: 0
        IdentitySource: method.request.header.Authorization
        AuthorizerUri:
          Fn::Join: ["",
            [
              "arn:aws:apigateway:",
              "${self:custom.region}",
              ":lambda:path/",
              "2015-03-31/functions/",
              Fn::GetAtt: ["YourFunctionNameLambdaFunction", "Arn" ],
              "/invocations"
            ]]
        RestApiId:
          Fn::ImportValue: ${self:custom.stage}-ApiGatewayRestApiId
        Name: api-${self:custom.stage}-authorizer 
        Type: REQUEST
    ApiGatewayAuthorizerPermission:
      Type: AWS::Lambda::Permission
      Properties:
        FunctionName:
          Fn::GetAtt: ["YourFunctionNameLambdaFunction", "Arn"]
        Action: lambda:InvokeFunction
        Principal:  
          Fn::Join: ["",["apigateway.", { Ref: "AWS::URLSuffix"}]]

  Outputs:
    AuthorizerRef:
      Value:
        Ref: ApiGatewayAuthorizer
      Export:
        Name: authorizer-ref:${self:custom.stage}

I hope you know how to add an API gateway and import it here like

RestApiId:
      Fn::ImportValue: ${self:custom.stage}-ApiGatewayRestApiId

, since it's already specified in the accepted answer

And In my case I passed value in the event header as Authorization type to get it in the authorizer lambda function and my type is REQUEST



回答5:

Changing to a shared custom API Gateway Lambda Authorizer was straightforward once it was working as part of the service. At that point it was just add an arn: to a deployed lambda (authorizer) and remove the "authorizer" definition from the service to a separate deployable service.

  myLambdaName:
    handler: handler.someNodeFunction
    name: something-someNodeFunction
    events:
      - http:
          path: /path/to/resource
          method: get
          cors: true
          authorizer: 
            name: myCustomAuthorizer
            # forwarding lambda proxy event stuff to the custom authorizer
            IdentitySource: method.request.header.Authorization, context.path
            type: request
            arn:  'arn:aws:lambda:region:##:function:something-else-myCustomAuthorizer'

Then the other "service" just has some custom authorizers shared by multiple deployed microservices.

functions:

  myCustomAuthorizer:
    name: something-else-myCustomAuthorizer
    handler: handler.myCustomAuthorizer

Side note:

If you'd want a token type authorizer, which does forward the "authorization: bearer xyzsddfsf" as a simple event:

{
    "type": "TOKEN",
    "methodArn": "arn:aws:execute-api:region:####:apigwIdHere/dev/GET/path/to/resource",
    "authorizationToken": "Bearer ...."
}
          authorizer: 
            arn:  'arn:aws:lambda:region:##:function:something-else-myCustomAuthorizer'