Networking & Content Delivery

Securing CloudFront Distributions using OpenID Connect and AWS Secrets Manager

Amazon CloudFront is a CDN that is used to securely deliver content, applications, and APIs to globally dispersed customers with low-latency and high transfer speeds. Amazon CloudFront is ideal for serving-up websites, caching content, and delivering static files to users across the globe. This blog post will allow organizations who host private web apps on Amazon CloudFront to limit access to only authenticated users thus preventing the direct access to Amazon S3 URL and providing them access by using the CloudFront URL to protect the data. In this post, you will integrate Amazon CloudFront and Lambda@edge with Identity Providers (Cognito, Okta) to perform OpenID Connect (OIDC) Authorization Code Flow.

1.    A user navigates to the CloudFront URL.
2.   CloudFront distribution receives the viewer request and triggers the Lamba@Edge function. The function checks a cookie to validate the user’s authentication information. If the user is signing in for the first time and there is no valid cookie present, the Lambda@edge function redirects the user to authenticate.
3.    The user’s web browser is redirected to IDP custom UI page to sign in and authenticate.
4.    After authentication, IDP generates and cryptographically signs a JWT (JSON Web Token) then responds with a redirect containing the JWT embedded in the URL Path.
5.    The viewer’s web browser extracts JWT from the URL and makes a request to the content, adding an Authorization request header with JWT as the value.
6.    If the verification steps pass, Lambda@Edge strips out the Authorization header and allows the request to pass through to designated origin for CloudFront. User is redirected to the IDP page to re-authenticate in all other cases.
7.    CloudFront passes the request through to the origin. In this case, the origin is the Amazon S3 bucket.
8.    After retrieving the content from the S3 bucket, CloudFront sends the response back to the browser and CloudFront caches the content for the specified TTL.
9.    The browser displays the response to the user.

Solution Deployment Pre-requisites

1.    Create a new AWS account or use an existing account.
2.    Create or use an existing S3 Bucket for the SAM packaging following these instructions.
3.    Install the AWS SAM (Serverless Application Model) CLI using these instructions.
4.    Install and start Docker following these instructions.
5.    Ensure you have a Git client installed following these instructions.
6.    Set up AWS Credentials in your environment using these instructions.

SAM Deployment Template

To get started, navigate to a terminal and run the following commands:

$ git clone https://github.com/aws-samples/lambdaedge-openidconnect-samples
$ cd lambdaedge-openidconnect-samples

$ export DEPLOY_BUCKET="YOUR_SAM_S3_BUCKET" # this is the S3 Bucket where to which SAM deploys the Lambda@Edge code
$ export STATIC_WEBSITE_BUCKET="YOUR_STATIC_ASSET_BUCKET" # new S3 bucket for content that will serve as the origin for CloudFront

$ export YOUR_LOG_BUCKET_NAME="YOUR_LOG_BUCKET_NAME" # S3 bucket for your Logs

$ export YOUR_SECRETS_MANAGER_KEY_NAME="YOUR_SECRETS_MANAGER_KEY_NAME" # Secret managerKey containing OpenID Connect configuration
$ echo "SAM BUILD"
$ sam build -b ./build -s . -t template.yaml -u

$ echo "SAM PACKAGE"
$ sam package \
 --template-file build/template.yaml \
 --s3-bucket ${DEPLOY_BUCKET} \
 --output-template-file build/packaged.yaml
 
$ echo "SAM DEPLOY"
sam deploy \
 --template-file build/packaged.yaml \
 --stack-name oidc-auth \
 --capabilities CAPABILITY_NAMED_IAM \
 --parameter-overrides BucketName=${YOUR_NEW_STATIC_SITE_BUCKET_NAME}
   LogBucketName=${YOUR_LOG_BUCKET_NAME} SecretKeyName=${YOUR_SECRETS_MANAGER_KEY_NAME}

Infrastructure created from SAM Template

Deploying the SAM template creates the following infrastructure:

Lambda@Edge Configuration

Below configurations are required to configure your IDP provider in Lambda@Edge, you will need to create the Base64 encoded configuration and place it into AWS Secrets Manager


{
"AUTH_REQUEST": {
"client_id": "${CLIENT_ID_FROM_IDP}",
"response_type": "code",
"scope": "openid email",
"redirect_uri": "https://${CLOUDFRONT_DIST_URL}/_callback"
},
"TOKEN_REQUEST": {
"client_id": "${CLIENT_ID_FROM_IDP}",
"redirect_uri": "https://${CLOUDFRONT_DIST_URL}/_callback",
"grant_type": "authorization_code",
"client_secret": "${CLIENT_SECRET_FROM_IDP}"
},
"DISTRIBUTION": "amazon-oai",
"AUTHN": "COGNITO",
"DISCOVERY_DOCUMENT": "https://${IDP_DOMAIN_NAME}/.well-known/openid-configuration",
"SESSION_DURATION": 30,
"BASE_URL": "https://${IDP_DOMAIN_NAME}/",
"CALLBACK_PATH": "/_callback",
"AUTHZ": "COGNITO"
}

Explanation of parameters for Lambda@Edge Configuration

When integrating with an Identity Provider (IdP), there are a common set of parameters that must be supplied for example:
·      callback_url = this is the URL that the IdP will redirect to after the authentication process is complete. In this case, we will supply the URL of the CloudFront distribution where we are exposing our application.
·      scope = what types of Authentication scopes will I allow? In this case we will supply openid and email scopes.
The IdP will provide two parameters back to you after registering an application with them:
·      client_id  = this is App Client ID used to perform OIDC Authorization Code Flow.
·      client_secret = this is the App Client Secret used to perform OIDC Authorization Code Flow.

Save Configuration to Secrets Manager

You can base64 encode the IDP configuration settings before putting them in Secrets Manager as shown in the following:

Details of Lambda@Edge Function

To follow along with the code, navigate to this link.

The Lambda@Edge function will first check if it has the required configurations it needs. If the secretId variable is undefined, it will reach out to AWS Secrets Manager to gather the configuration from AWS Secrets Manager using the pattern from our previous blog.

Next, the Lambda@Edge function checks the request for a cookie containing a valid JSON Web Token (JWT). If the JWT token exists, a call is made to verify the JWT token.

If the token does not exist, or is invalid, the user is redirected to the login page provided by the identity provider and the function below is called to redirect them to OpenID Connect (OIDC) provider.

When the user successfully logs in, they are redirected back to the site using a predefined callback URL, Lambda@Edge begins processing the login response, retrieves a JWT from the identity provider, sets this JWT as a cookie, and finally, redirects the user to their original request URL.

At this point, the user requests have a valid cookie so our Lambda@Edge function processes the request by CloudFront and retrieves content from the S3 bucket.

Conclusion

In this blog, you have learned how to secure your Amazon CloudFront distribution with a Lambda@Edge viewer request. In addition, you have also learned how to use AWS SAM to automate the setup and deployment of the infrastructure outlined in the architecture above. Finally, you have used secure access patterns with AWS Secrets Manager for retrieving the OpenID Connect configuration at an edge location.

Thank You

A special Thank You to David Brown (Senior CloudFront Product Manager) for his guidance, technical depth, and perspective in regards to the content contained within this blog.

About the Authors

Viyoma Sachdeva

Viyoma Sachdeva is a DevOps Consultant in Amazon Web Services supporting Global Customers and their journey to cloud. Outside of work she enjoys watching series and spending time with her family

Matt Noyce

Matt Noyce is a Cloud Application Architect in Professional Services at Amazon Web Services.
He works with customers to architect, design, automate, and build solutions on AWS
for their business needs.