Networking & Content Delivery

Authorization@Edge – How to Use Lambda@Edge and JSON Web Tokens to Enhance Web Application Security

by Alex Tomic and Cameron Worrell

Authorization, the function of specifying access rights to resources is often required to help protect restricted content in web applications. This post will show you how to implement a serverless authorization of viewers using Amazon CloudFront, Lambda@Edge and Amazon Cognito without modifying your origin resources. To learn more about edge networking with AWS, click here.

Amazon CloudFront is a global content delivery network (CDN) service that securely delivers data, videos, applications, and APIs to your viewers with low latency and high transfer speeds. Lambda@Edge lets you run AWS Lambda functions in an AWS location close to your customer in response to CloudFront events, without provisioning or managing servers. Your code can be triggered by Amazon CloudFront events such as requests for content by viewers or requests from CloudFront to origin servers. You can use Lambda functions to change CloudFront requests and responses at the following points:

  • After CloudFront receives a request from a viewer (viewer request)
  • Before CloudFront forwards the request to the origin (origin request)
  • After CloudFront receives the response from the origin (origin response)
  • Before CloudFront forwards the response to the viewer (viewer response)


Amazon Cognito lets you easily add user sign-up and sign-in to your mobile and web applications. In addition, Amazon Cognito supports OAuth 2.0 as an industry standard protocol for authorization, and the sample application in this blog post relies on JSON Web Tokens to authorize access to private content. JSON Web Token (JWT) is a JSON-based open standard for creating access tokens which assert a series of claims as a JSON object. JSON Web Tokens can also be signed using private/public key pairs in order to verify content authenticity and integrity.

There are several benefits to using Lambda@Edge for authorization operations. First, performance is improved by running the authorization function using Lambda@Edge closest to the viewer, reducing latency and response time to the viewer request. The load on your origin servers is also reduced by offloading CPU-intensive operations such as verification of JSON Web Token (JWT) signatures. Finally, there are security benefits such as filtering out unauthorized requests before they reach your origin infrastructure.

Solution overview

This blog post includes a sample application to demonstrate how you can use Lambda@Edge to authorize viewer requests. In order to fully demonstrate the functionality, the solution also uses Amazon Cognito and Amazon S3.

The following diagram depicts a high-level overview of this posts solution:

Here is how the solution works:

  1. The viewer’s web browser is redirected to Amazon Cognito custom UI page to sign up and authenticate.
  2. After authentication, Cognito generates and cryptographically signs a JWT then responds with a redirect containing the JWT embedded in the URL.
  3. The viewer’s web browser extracts JWT from the URL and makes a request to private content (private/* path), adding Authorization request header with JWT.
  4. Amazon CloudFront routes the request to the nearest AWS edge location. The CloudFront distribution’s private behavior is configured to launch a Lambda@Edge function on ViewerRequest event.
  5. Lambda@Edge decodes the JWT and checks if the user belongs to the correct Cognito User Pool. It also verifies the cryptographic signature using the public RSA key for Cognito User Pool. Crypto verification ensures that JWT was created by the trusted party.
  6. After passing all of the verification steps, Lambda@Edge strips out the Authorization header and allows the request to pass through to designated origin for CloudFront. In this case, the origin is the private content Amazon S3 bucket.
  7. After receiving response from the origin S3 bucket, a JSON file in this example, CloudFront sends the response back to the browser. The browser displays the data from the returned JSON file.

Let’s dive deeper into the data flow for this solution…

This solution uses Amazon CloudFront to reduce latency and accelerate performance. This entails routing of viewer requests to the nearest edge location, static content caching and optimizations for dynamic content. The web application’s static elements are stored in Amazon S3, taking advantage of its close integration with Amazon CloudFront. Amazon S3 buckets will contain the web application as well as the private data. The private data will be stored in JSON format in the private S3 bucket.

Before application access is authorized using Lambda@Edge, viewers will first be identified and authenticated. Viewers will authenticate against Amazon Cognito User Pool and obtain a JWT. The viewer’s browser will then send the JWT in the Authorization header. CloudFront will invoke Lambda@Edge in response to the incoming ViewerRequest event. Lastly, the Lambda@Edge function will decode the JWT and verify its signature.

Requests without Authorization headers containing a valid JWT will result in Lambda@Edge responding with a “401 unauthorized” error message. The function takes advantage of response-generating capability of Lambda@Edge to return immediate responses for invalid requests without causing additional load on the origin server. This is one example of how authorization at edge can improve the security posture of your solution.

Requests with a valid JWT that pass through all the verification steps are sent to the “private” Amazon S3 bucket. For those valid requests, the function takes advantage of another Lambda@Edge capability: header manipulation. Lambda@Edge can read, modify, and delete request headers, including cookies.  After JWT verification is completed, the Authorization header is removed before passing the request to origin. This is useful because Amazon S3 cannot handle Authorization headers with JSON Web Tokens. Instead, CloudFront uses Origin Access Identity authentication to retrieve private content from S3 buckets.

Deployment

1.      Launch Stack

Click the “Launch Stack” button below to launch a CloudFormation stack in your account. Note that the stack will launch in the N. Virginia (us-east-1) region. It takes approximately 15 minutes for the CloudFormation stack to complete.

2.      Locate Lambda@Edge Function

The next step is to publish the Lambda@Edge function.

  1. From the AWS Management Console, choose Services > CloudFormation
  2. Select auth-at-edge stack
  3. In the Outputs section, look for LambdaAtEdgeFunction with the URL for editing the Lambda function, similar to the screenshot below.
  4. Click on the link and you will be redirected to the Lambda console, with the Lambda function already open, similar to this:

3.      Publish Lambda@Edge Function

For a function to be used by Lambda@Edge, it must be published first.

  1. Click on that function to open its properties. Under Actions, select Publish new version.
  2. You can type in any Description, then click on Publish. Copy the published functions ARN to clipboard or to a text editor.

ARN value should end with :1 (version 1). See example below:

4.      Configure CloudFront Distribution

You can now associate published Lambda function with the CloudFront distribution.

  1. From the AWS Management Console, choose Services > CloudFormation
  2. Select auth-at-edge stack
  3. In the outputs section, look for CloudFrontDistribution with the URL for editing the Lambda function, similar to the screenshot below.
  4. Click on the link and you will be redirected to the CloudFront console, with the Lambda function already open, similar to this:
  5. Switch to the Behaviors tab, select private/* behavior and click Edit.

6. Scroll to the bottom to edit Lambda Function Associations. For Event Type, select Viewer Request and paste the correct value for Lambda Function ARN (from step 3.2).

7. Finally, click on Yes, Edit to submit changes to your CloudFront distribution. Changes will take 10-15 minutes to complete.

That’s it, you are now ready to test Authorization @Edge!

Testing the solution

To test this architecture, you will first validate the security provided by the Lambda@Edge function against an unauthenticated session. Then you will sign up as a user in Amazon Cognito, authenticate, and successfully view the private content. Here are the detailed steps to complete the testing.

1.      Locate Application

First, navigate to CloudFormation stack you created earlier. One of the outputs is MAINURL.

That link takes you to the web application – private content viewer that provides a simple view of JWT and private content:

2.      Verify Access Blocking

Note that you currently don’t have appropriate JWT since you haven’t logged in yet. To verify that Lambda@Edge is protecting the private content and blocking unauthorized requests, click on Retrieve Private Data button. You should see an alert dialog popup noting that Lambda@Edge has blocked your access:

3.      Obtain a JSON Web Token from Amazon Cognito

To gain access to private data, you have to authenticate first. Click on Authenticate with Amazon Cognito button. You will be presented with Amazon Cognito Custom UI:

Click on Sign up and follow instructions to register a new username, password, and verify your email address. Note that as part of the verification process, you will need to copy the one-time code sent to your email. Once authenticated, your browser will redirect back to the Private Content Viewer page, but this time you will have a JSON Web Token:

4.      Retrieve Private Data

You now have credentials which are asserted in the JWT and can use them to retrieve private content. Click on Retrieve Private Data button and review results:

Success! Your request was successfully authorized by Lambda@Edge function and private content is now displayed in your browser.

Note: to delete the resources provisioned by the CloudFormation template in this post, you will need to delete the Cognito user pool, the private and the public S3 buckets detailed in the stack outputs, and the CloudFormation stack.

Conclusion

In this blog post, you learned to use Lambda@Edge to implement authorization based on JSON Web Tokens issued by Amazon Cognito. This solution represents one example of a variety of possible use cases where you can take advantage of Lambda@Edge. By moving components of your application closer to your viewers, you can enhance both the performance and security of your web applications. And best of all, you can take advantage of Lambda@Edge without deploying or modifying server infrastructure. The source code for this solution is available on GitHub. If you have questions about or issues implementing this solution, start a new thread in the CloudFront Forum, Cognito Forum or contact AWS Support.

Alex and Cameron