DEV Community

Cover image for Build Auth Once With A Shared Lambda Authorizer
Allen Helton for AWS Community Builders

Posted on • Originally published at readysetcloud.io

Build Auth Once With A Shared Lambda Authorizer

Everybody has opinions on how AWS accounts should be used.

Some people think you should have a mono-account and store everything in your AWS ecosystem in one place.

Others believe an individual application (composed of multiple microservices) belong in a single AWS account.

And others take it to the extreme and keep a single microservice in an AWS account.

None of these approaches are inadvertently wrong (except you might run into some resource limitations with a mono-account), but they all run into the same problem:

How do you maintain consistent authorization across accounts?

If you use a custom Lambda authorizer, the answer is both simple and not-so-simple.

What is a Lambda Authorizer?

Chances are, if you chose to read this article you already know what a Lambda authorizer is.

But as a light refresher, a Lambda authorizer is an API Gateway feature that uses a Lambda function to perform authorization for calls into your API.

It can authenticate an OAuth or SAML token, apply some business logic to determine access, and anything in between.

For serverless applications, it can be useful to have a Lambda authorizer sit on top of your API gateway to help fine-tune control to your individual endpoints.

Defining a Lambda Authorizer in SAM

The Serverless Application Model (SAM) is a layer on top of CloudFormation designed to make the definition of serverless applications simple and easy.

For easiest consumability and portability, all the examples in this post will be defined in a SAM template so you may take what you learn and immediately deploy it in your own AWS account. No need to fumble through the console.

A Lambda authorizer is just a function. There's nothing special to how it is declared. It just expects a different event body than a Lambda proxied by API Gateway.

In my example repo, we define a Lambda authorizer like this:

LambdaAuthorizerCrossAccountFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: lambdas/lambda-authorizer
    Runtime: nodejs12.x
    Handler: lambda-authorizer.lambdaHandler
    Role: !GetAtt LambdaAuthorizerRole.Arn
    FunctionName: LambdaAuthorizer      
Enter fullscreen mode Exit fullscreen mode

The example repo has the SAM template, but not the code to implement the authorizer. For examples on building the authorizer itself, AWS has blueprints on GitHub.

With the authorizer function defined, the next step is to enable permissions for other accounts to use it.

Building Authorizer Permissions

Cross-account permissions can be tricky with AWS. Permissions need to be defined in the source account to allow consumers to access their resources. This means that you must know your consumers ahead of time (which is honestly a good thing).

With Lambda authorizers, permissions are straight forward. You must give API Gateway in the consumer accounts permission to execute the authorizer function.

Diagram of how the Lambda authorizer is consumed by API Gateway in other accounts
Diagram of how the Lambda authorizer is consumed by API Gateway in other accounts

In our SAM template, the permission needed is defined as:

ConsumerOneAuthorizerPermission:
  Type: AWS::Lambda::Permission
  Properties:
    Action: lambda:InvokeFunction
    FunctionName: !Ref LambdaAuthorizerCrossAccountFunction
    Principal: apigateway.amazonaws.com
    SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${ConsumerOneAccountId}:*/authorizers/*
Enter fullscreen mode Exit fullscreen mode

This snippet is saying API Gateway, the principal, is allowed to invoke a function with a specific name. The SourceArn is stating which resource is allowed to execute the function. I have parameterized the account id along with the AWS partition (commercial or GovCloud) and region.

Each consuming account will need a permission giving it access to execute the authorizer. Since we're practicing POLP, this seems like a worthy tradeoff since we will have to maintain this over time as we bring on new accounts to the organization.

Consuming The Authorizer In Another Account

This is where the fun starts. With the authorizer built and permissions created, we can set up our consuming accounts to start using it.

In the SAM template of one of our consuming accounts, the serverless API and authorizer are defined with the following snippet:

ConsumerServiceApi:
  Type: AWS::Serverless::Api
  Properties:
    StageName: test
    Auth:
      DefaultAuthorizer: LambdaAuthorizer
      AddDefaultAuthorizerToCorsPreflight: false
      Authorizers:
        LambdaAuthorizer:
          FunctionPayloadType: REQUEST
          FunctionArn: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AuthorizerAccountId}:function:LambdaAuthorizer
          Identity:
            Headers:
              - Authorization
            ReauthorizeEvery: 3600
Enter fullscreen mode Exit fullscreen mode

The major piece to note here is the FunctionArn we are composing. For security, we store the AuthorizerAccountId in a parameter and pass it in at deploy time so account ids are not in the source code.

This is similar to what was done above when we were defining the authorizer itself. It assembles the arn and uses that to point to the resource we want.

When this template is deployed (see deployment instructions) a Lambda authorizer will be created in the consumer account and the function it uses lives in the authorizer account.

This brings me to the final piece: a workaround for a defect in SAM.

Fixing the Glitch

A SAM template is just a fancy CloudFormation script that has a bunch of aliases that are transformed at deploy time. SAM provides nice, easy shortcuts to create serverless functions and triggers to said functions.

On deploy time, a Transform is called that is maintained by AWS to take the shortcuts written in the spec and turn them into full CloudFormation resources. It's a much easier way to write infrastructure as code.

Unfortunately, there is a bug in this transform that prevents us from consuming a Lambda authorizer in a different account.

SAM tries to configure a lambda permission from the authorizer in the consumer account to the function in the authorizer account. Due to how IAM works, setting up permissions that direction doesn't work.

Diagram of how IAM permissions must be created when giving cross-account access
Permissions to cross-account resources must be declared in the account where the resource lives

SAM tries to be nice and configures the permission needed for the authorizer to invoke the function. But it doesn't know that function lives in another account, so the permission it creates will fail the deployment.

To get around this issue, we can create a CloudFormation macro to remove the permission automatically.

Macros are functions that run on deployment to transform resources for you automatically. In this case, we want to remove a permission resource generated by SAM.

I created a macro in NodeJS adapted exactly from the example in the reported GitHub issue and added it to the prerequisite stack in the example repo.

Once you create and deploy the macro, all stacks that consume the cross account authorizer must be updated to use it.

In your SAM template, there is a Transform property that already calls on the AWS SAM macro. To add the Lambda authorizer macro, we can make this property an array and it will begin to run on deployment.

AWSTemplateFormatVersion: '2010-09-09'
Transform: [ AWS::Serverless-2016-10-31, RemoveAuthorizerLambdaPermissions ]
Description: >
  Consumer Stack. This AWS Account will consume the authorizer created in the *authorizer stack*
Enter fullscreen mode Exit fullscreen mode

With the macro in place, the consumer stack will deploy and you have yourself a cross-account Lambda authorizer!

Final Thoughts

Reusing code and resources is key to maintainability at scale. Try to reuse what you can whenever it makes sense. Authorization/Authentication are perfect examples of reusability that also provides a cohesive feel to your applications.

Everybody wants to reinvent the wheel and build it themselves. That's the nature of developers. Getting to the point where you are consuming code, resources, APIs, and other features across your organization takes governance, it isn't just a technical problem.

Removing the technical hurdles is step one. Step two is putting it in action.

I have shown you step one for maintaining consistent Auth in your software ecosystem.

Now it's your turn.

Top comments (2)

Collapse
 
art_wolf profile image
John Doyle

This is, honestly, fantastic! I had never thought of centralizing the authorizer function - but it makes the system more secure, and far simpler to develop with!

Collapse
 
allenheltondev profile image
Allen Helton

Thank you! I just went through this whole process and it's an absolute game changer :)