Authentication at the edge with Lambda@Edge and Cognito
For many years, we used a hosting partner for serving the Rapid Docker on AWS Video Course. When someone bought the video course, we created a user account with our partner. The hosting partner provided a website to watch the videos and a login form. For many reasons, we migrated the video course to a new home -our home- cloudonaut.io. In this article, I outline the architecture, the user flow, and go through code snippets to implement user authentication at the edge with Lambda@Edge functions and a Cognito user pool.
This post is not about video hosting on AWS. We cover video hosting on AWS in this post.
From a high-level perspective, the following components are used:
- Cognito user pool stores users, hosts login UI, and issues JWT tokens.
- Lambda@Edge functions check if the request contains a cookie with a valid JWT token and implement a tiny backend to implement the OAuth 2.0 Authorization Code Flow.
- CloudFront distribution delivers the content to the end-users and triggers Lambda@Edge functions.
- S3 bucket stores the content served by CloudFront.
The following figure shows the high-level architecture.
Let’s continue with the user perspective.
The user starts the journey by visiting a protected web page. The following figure shows what happens next.
The following steps are executed:
- The user visits a protected page (e.g., https://cloudonaut.io/rapid-docker-on-aws/video-course/ch00-01.html) hosted on CloudFront.
- CloudFront invokes the viewer request Lambda@Edge function.
- The function inspects the cookie header, extracts the cookie named
token, and verifies the value to check if it is a valid JWT token issued by Cognito.
- Let’s assume no cookie is present. The Lambda@edge function generates an HTTP 302 response to redirect to the Cognito hosted UI.
- The user’s browser follows the redirect and loads the Cognito hosted UI with a login screen.
- Once the user enters a valid username and password, Cognito returns an HTTP 302 response to redirect to the cloudonaut.io backend (
- The user’s browser follows the redirect and loads the backend. In this case, we implement the backend with an origin request Lambda@Edge function.
- The function exchanges the received authorization code (query parameter
code) with an access token (in JWT format). An HTTP 302 response is generated with the
- The user’s browser stores the
tokencookie and follows the redirect with the
Cookieheader containing the access token. The user can access the protected page.
For a better understanding, let’s dive into Lambda@Edge.
When CloudFront receives a request, it can invoke the so-called viewer request Lambda@Edge function. You can inspect the HTTP headers at this point and generate HTTP responses (such as a 302 redirect). The viewer request function execution is limited to 5 seconds and 128 MB of memory. The function’s code and libraries must fit into a 1 MB zip file.
The snippets hardcode the region to eu-west-1. If your region is different, replace all occurrences of eu-west-1!
The following snippet shows the implementation to check if a cookie
token is part of the HTTP header and if the value is a signed JWT issued from Cognito.
Looking for a new challenge?
// required libraries
When the viewer request function does not generate a response, the request is passed to the CloudFront origin (e.g., an S3 bucket).
When CloudFront can not serve the request from the cache, the origin request Lambda@Edge function is invoked just before the request to the origin is made. The main difference is that this function can take up to 30 seconds, use as much memory as Lambda offers, and the uploaded code archive can be up to 50 MB.
In our case, we use a Lambda@Edge function to implement a tiny backend. The backend exchanges an authorization code with an access token and ensures that the response is not cachable by setting the
Cache-Control header to
Feel free to implement this part using an API Gateway or similar.
// required libraries
The following CloudFormation template describes the Cognito user pool, client, and domain infrastructure needed.
To get the client secret, we use the following bash snippet in our deployment pipeline:
COGNITO_STACK_NAME="" # TODO fill with your stack name
origin-request-src) to store the Lambda@Edge function code (
lambda.js) together with a
To deploy the stack in
us-east-1 (region required my Lambda@Edge), run:
aws --region us-east-1 cloudformation package --s3-bucket YOUR_S3_ARTIFACT_BUCKET_NAME --template-file YOUR_TEMPLATE_FILE_NAME.yaml --output-template-file output.yaml
The CloudFront infrastructure can be deployed with our Free Templates for AWS CloudFormation. Set the parameters
OriginRequestLambdaEdgeFunctionVersionARN to the values from the stack you deployed before containing the Lambda@Edge functions.
One modification to the
static-website.yaml template is needed. Inside the
You can add authentication to any website served by CloudFront by using Lambda@Edge. You can set up a Cognito user pool if you want to use your own identity provider. The described flow works with any other identity provider as long as you receive a JWT access token.
PS: You can even add simple authorization using Cognito user groups. If you add a Cognito user to a group, the group name will show up in the
cognito:groups claim in the JWT access token.