DEV Community

Arpad Toth for AWS Community Builders

Posted on • Originally published at arpadt.com

Authenticating users in the load balancer with Cognito

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
Enter fullscreen mode Exit fullscreen mode

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.

Enabling authentication in the load balancer

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

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)