Protecting Static Websites with Cognito in AWS
S3 buckets are great for hosting static web pages. However, that makes your content publicly accessible, too. One way to protect your pages is by adding authorization on top of them. With this project, using a CloudFront distribution, Lambda@Edge functions, and a Cognito user pool; a user login page and JWT authorization logic will be implemented in front of your static website in S3. Also, this system will make sure the JWT token is saved into browser cookies once a user logged in so that the user will not need to re-login on its next attempts. AWS CDK is used as IaC (Infrastructure as Code) tool in this project to provision all above-mentioned resources into the AWS cloud programmatically. By changing some parameters regarding your needs, you can deploy the same solution into your account as well without bothering on the AWS console.
Solution Architecture
The following diagrams represent the behaviors of the system.
1. Unauthenticated User Tries to View Website
CloudFront sits in front of your S3 bucket and receives all the requests. It’s possible to intercept requests and responses with a special type of Lambda function (Lambda@Edge). Here, in this example, the default handler function intercepts all viewer requests except the ones made into /callback path. Its responsibility is to check whether a JWT exists in the client’s browser or not. In this case, the user does not have a JWT since it has not logged in yet. So the default handler function gets the login page URL from the SSM parameter store and returns an HTTP 301 response to redirect the client to a login page (Cognito user pool hosted UI).
2. Login Flow
Cognito user pools are simply user databases for your web and mobile applications in which you can implement OAuth flows for these users’ authorization. You can even connect any identity provider to your user pools so that your users can sign in with familiar services such as Google login, Facebook login, Okta login, etc. But in this example, for the sake of simplicity, no identity provider is integrated. Instead, email and password-based simple authentication is implemented. Cognito user pool also allows you to create user pool clients so that you can have an AWS-generated hosted UI page your users can visit to sign in. Users can obtain a code from Cognito as long as their credentials are valid.
Suppose our client has logged in successfully and redirected to the callback URL by Cognito. In the query string parameters of callback URL, authorization code and a particular query string parameter called state can be found, in the previous flow Default Handler function had embedded the requested URL into that state parameter. So here Callback Handler receives the request. It extracts the requested URL from the state query string parameter. Fetches some necessary credentials and parameters from SSM Parameter store and exchanges authorization code with a JWT (ID token) against Cognito Token endpoint. Finally, once the JWT is obtained, it saves the JWT into browser cookies and redirects the client back to the requested URL.
3. Authenticated User Tries to View Website
At this step, similar to the 1st one, the request is intercepted by the Default Handler once more. Since the user now has the JWT token in its cookies, the Default Handler validates the JWT against the Cognito user pool client. As long as the provided JWT is valid and not expired, the Default Handler returns the request intact to CloudFront. At this point, you may consider using an access token instead of an ID token and implementing any additional custom authorization logic based on the claims provided in that JWT but for the sake of simplicity we assumed that there is only one type of authenticated users and it’s enough to authorize them as long as their ID token is verified against the issuer. Finally, CloudFront gets the requested object (index.html file) from the S3 bucket and returns it to the client.
Deploying This Application to AWS
1. Ensure you have installed Docker and AWS CDK on your computer.
Here are some quick links if you don’t have them installed yet:
2. Make sure that you have your AWS CLI credentials, they have sufficient permissions to create bootstrapping and deployment of the CDK stack.
If you don’t have your AWS CLI credentials yet, visit the following link and create your Access Key and Secret Access Key under the My Security Credentials menu at IAM Console.
https://console.aws.amazon.com/iam?p=iam&cp=bn&ad=c
3. Bootstrap the CDK Application
Open a terminal at the root directory of the project and run the following command:
$ cdk bootstrap
4. Deploy the CDK Application
Open a terminal at the root directory of the project and run the following command:
$ cdk deploy --context CDK_AWS_ACCOUNT_ID={your aws account ID} --context CDK_ENV={your environment name e.g. staging or prod} --context CDK_APP={your app. name}
Once the build artifacts become ready to be deployed, CDK will prompt you for your consent to execute the changeset.
Allow changeset execution, and then you can keep track of the deployment status live in your terminal.
Links
- Code Repository: https://github.com/onanmco/protected-s3-static-website