Authenticating users in the load balancer with Cognito
Tags: aws, security, serverless
Published: 2023-09-21 06:57:24

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 <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html" class="link" target="_blank" rel="noopener noreferrer">Application Load Balancer (ALB)</a>. The load balancer's target can be any supported target, including <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html" class="link" target="_blank" rel="noopener noreferrer">ECS containers</a>, <a href="https://aws.amazon.com/ec2/" class="link" target="_blank" rel="noopener noreferrer">EC2 instances</a> or even <a href="https://aws.amazon.com/lambda/" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> 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 <a href="https://openid.net/developers/how-connect-works/" class="link" target="_blank" rel="noopener noreferrer">OIDC</a> compliant identity providers, social and corporate identities.

<a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html" class="link" target="_blank" rel="noopener noreferrer">Cognito User pools</a> 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

<ul class="list">
  <li class="list__item">an ALB</li>
  <li class="list__item">target groups</li>
  <li class="list__item">a user pool, hosted UI, and app client in Cognito.</li>
</ul>

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 <a href="https://aws.amazon.com/route53/" class="link" target="_blank" rel="noopener noreferrer">Route 53</a> 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 <a href="https://aws.amazon.com/certificate-manager/" class="link" target="_blank" rel="noopener noreferrer">Certificate Manager</a> 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.

![Enabling authentication in the load balancer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zjs363zgyna7a74lindh.png)

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:

![Adding scopes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jf8l52dcfnzfu1d5smvp.png)

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.

```jsonc
{
  "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:

```json
{
  "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. <a href="https://dev.to/aws-builders/using-cognito-groups-to-control-access-to-api-endpoints-346g" class="link" target="_blank" rel="noopener noreferrer">This post</a> 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 <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">API Gateway</a> does when it authenticates users with Cognito <a href="https://dev.to/aws-builders/implementing-access-control-on-api-gateway-endpoints-with-id-tokens-k9p" class="link" target="_blank" rel="noopener noreferrer">ID tokens</a>. 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

<a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-application-load-balancer.html" class="link" target="_blank" rel="noopener noreferrer">Create an Application Load Balancer</a> - How to create an ALB

<a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-application-load-balancer.html" class="link" target="_blank" rel="noopener noreferrer">Create a target group</a> - How to create a target group

<a href="https://docs.aws.amazon.com/cognito/latest/developerguide/tutorial-create-user-pool.html" class="link" target="_blank" rel="noopener noreferrer">Tutorial: Creating a user pool</a> - How to create a user pool

<a href="https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html" class="link" target="_blank" rel="noopener noreferrer">Configuring a user pool app client</a> - What the title of the documentation says

<a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html" class="link" target="_blank" rel="noopener noreferrer">Authenticate users using an Application Load Balancer</a> - Detailed description of the authentication flow and configuration options