Authenticating users in the load balancer with Cognito
Arpad Toth
Posted on September 21, 2023
We can configure Application Load Balancer to authenticate application users with Cognito. By enabling the feature in the listener rule, we can offload user identification to the load balancer and create an automatic authentication process.
1. The scenario
Say that we have an application running behind a public-facing Application Load Balancer (ALB). The load balancer's target can be any supported target, including ECS containers, EC2 instances or even Lambda functions. Because the application is only available to authenticated users, we want to find a solution to identify them.
One way to solve this problem is to configure the ALB to authenticate users. ALB supports OIDC compliant identity providers, social and corporate identities.
Cognito User pools meets the above criteria, so we can configure the load balancer to use it for authentication. When we do so, the ALB will call the relevant Cognito endpoints to validate the user's identity.
Let's see how we can do it.
2. Pre-requisites
This post won't explain how to create
- an ALB
- target groups
- a user pool, hosted UI, and app client in Cognito.
I'll only highlight some key configuration options and provide you with some links at the end of the post that will help provision these resources.
3. Expected flow
First, we configure the ALB to authenticate users with the help of Cognito. Then, when the user calls a protected endpoint, the ALB will redirect them to the hosted UI. The user will then enter their credentials, and upon their successful authentication, they will see the values returned by the load balancer's target. The ALB calls the relevant endpoints in Cognito to validate the user's identity and retrieve the tokens.
4. Cognito user pool configuration
We should discuss a couple of configurations in the user pool.
4.1. App client secret
We need to generate a client secret in the app client. If we don't do so, we won't be able to set up authentication in the load balancer rule configuration. As the name indicates, the client secret is confidential. But it won't be visible anywhere throughout the authentication flow because everything happens in the background.
If we already have an app client without a secret generated, we should create a new one. We can't add a secret to an existing app client.
4.2. Hosted UI settings
Here, we need to configure a few things.
Callback URL
We should add a specific URL containing the custom domain pointing to the ALB to the Allowed callback URLs list:
https://MY_CUSTOM_DOMAIN/oauth2/idpresponse
In this case, we configure MY_CUSTOM_DOMAIN
to be an alias A record in Route 53 with the load balancer being the target value.
Oauth 2.0 grant types
We choose Authorization code grant here. In this flow, Cognito won't return tokens directly to the client. Instead, it will issue a code. The code-to-token conversion occurs in the background, so we'll see only the code in the browser but not the tokens.
Scopes
We need to select the openid
scope as a minimum. This way, the ALB can get an ID token from Cognito, which it needs for user authentication. We can also add custom scopes if we want to control access to specific paths (more on that below).
5. Load balancer configuration
Let's take a look at the load balancer configurations.
5.1. HTTPS listener
Only HTTPS listeners support authentication with Cognito, so we should create one.
Because of that, we'll need a valid public certificate, which we can request in Certificate Manager for free.
5.2. Rule settings
We can set up rules for specific paths in an ALB and configure different targets for each route. We may want to protect them with different scopes! We create a rule and add the path
condition (for example, /tasks/*
), then we can enable the authentication.
Next, we select Cognito as Identity provider, and select the user pool and the app client. We can set up the scopes in the Advanced authentication settings part:
We should always request the openid
scope because that's how Cognito will return an ID token.
We can also add other OIDC scopes like email
, phone
, or profile
. If we want to protect paths because, for example, we take advantage of the ALB's path-based routing feature, we can request custom scopes, too. Here, we specify the tasks/read
custom scope.
6. What happens next?
If we have only the default rule with one path or configured path-based routing, the ALB will redirect the user back to the original URL with a cookie. In this case, it's called AWSELBAuthSessionCookie
, and the ALB will validate it on each request.
But that's not all. The load balancer will create a couple of headers and forward them to the backend. They are all available from the input event.headers
object.
The x-amzn-oidc-accesstoken
header contains the access token in JWT format that Cognito issues at authentication.
{
"sub": "2cfef24c-6a87-45bb-b369-09f48ce12855",
"client_id": "APP_CLIENT_ID",
"token_use": "access",
"scope": "openid tasks/read", // <-- here are the requested scopes
"username": "USERNAME"
// other properties
}
The token contains the scopes that we can use for path validation at the backend if we have to.
The x-amzn-oidc-identity
contains the user's Cognito user pool ID called sub
. We can again use this value for validation if the use case justifies it.
Lastly, the x-amzn-oidc-data
carries the user claims (email, username, etc.) and the load balancer's signature. It's also a JWT whose header looks like this:
{
"typ": "JWT",
"alg": "ES256",
"iss": "https://cognito-idp.eu-central-1.amazonaws.com/USER_POOL_ID",
"client": "APP_CLIENT_ID",
"signer": "LOAD_BALANCER_ARN",
"exp": 1695126483
}
We can validate if the request comes from the load balancer by inspecting the signature in the header. This post shows a code example of how to do it.
7. Considerations
The ALB performs user authentication only. It checks if the user is indeed someone our application should know. In this regard the principle is similar to what API Gateway does when it authenticates users with Cognito ID tokens. It identifies the user and verifies that the user is legitimate.
But the process doesn't do authorization. If we need to control access to the load balancer endpoint or some paths, we can use the access token at the backend to perform the validation there. In this case, we can request specific scopes in the path's rule settings and check their presence in the access token in the backend code.
8. Summary
Application Load Balancers can authenticate users with the help of Cognito. We must create an HTTPS listener and configure the authentication in the ALB. We also need to request a scope that makes Cognito return an ID token, which the load balancer uses for authentication.
We can control access to specific paths with custom scopes if the business case requires it. In this case, we should check in the backend code if the required claim is present in the access token.
9. References, further reading
Create an Application Load Balancer - How to create an ALB
Create a target group - How to create a target group
Tutorial: Creating a user pool - How to create a user pool
Configuring a user pool app client - What the title of the documentation says
Authenticate users using an Application Load Balancer - Detailed description of the authentication flow and configuration options
Posted on September 21, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.