Networking & Content Delivery

Security best practices when using ALB authentication

At AWS, security is the top priority, and we are committed to providing you with the necessary guidance to fortify the security posture of your environment. In 2018, we introduced built-in authentication support for Application Load Balancers (ALBs), enabling secure user authentication as they access applications. This feature allows developers to offload the authentication responsibility from the backend target, which eliminates the need for writing complex authentication code and simplifying the application development process. To maintain a robust security environment, it is crucial to make sure that your applications and targets are correctly configured.

In the Amazon Web Services (AWS) shared responsibility model, AWS is responsible for the ‘Security of the Cloud,’ encompassing the security aspects of the underlying infrastructure and services, including the load balancer software itself. Specifically, AWS makes sure of the secure operation of the authentication mechanisms used by the ALB. On the other hand, users are responsible for ‘Security in the Cloud,’ which refers to the secure configuration of the resources they provision and use. This includes configuring parameters related to the ALB’s authentication feature, security group settings for target resources, and secure configuration of software running on target resources behind the ALB. Although the ALB handles request authentication, users must configure their application to process requests originating from the ALB securely.

In this post, we share our best practices to help you use the authentication capabilities of ALBs effectively and also make sure that robust security measures are in place. We dive deep into the best practices for enhancing the security of your environment with ALB authentication. Together, we can build a more secure and resilient digital ecosystem, safeguarding your data and applications from potential threats.

ALB authentication configuration

When configuring authentication, you set up an authenticate action for one or more listener rules. The authenticate-cognito and authenticate-oidc action types are supported only with HTTPS listeners, because authentication cannot be used for non TLS HTTP workloads. This is an example of a listener default rule:

        "Priority": "default",
        "Conditions": [],
        "RuleArn": "arn:aws:elasticloadbalancing:<REGION>:<ACCOUNT-ID>:listener-rule/app/alb/AAA/BBB/CCC",
        "IsDefault": true,
        "Actions": [
            {
                "AuthenticateOidcConfig": {
                    "OnUnauthenticatedRequest": "authenticate",
                    "TokenEndpoint": "https://<YOUR_TOKEN_ENDPOINT>/<PATH>",
                    "ClientId": "<YOUR_CLIENT_ID>",
                    "SessionTimeout": 604800,
                    "AuthorizationEndpoint": "https://<YOUR_AUTHORIZATION_ENDPOINT>/<PATH>",
                    "Scope": "openid",
                    "SessionCookieName": "AWSELBAuthSessionCookie",
                    "UserInfoEndpoint": "https://<YOUR_USERINFO_ENDPOINT>/<PATH>",
                    "Issuer": "https://<YOUR_ISSUER_ENDPOINT>/<PATH>"
                },
                "Type": "authenticate-oidc",
                "Order": 1
            },
            {
                "ForwardConfig": {
                    "TargetGroupStickinessConfig": {
                        "DurationSeconds": 3600,
                        "Enabled": false
                    },
                    "TargetGroups": [
                        {
                            "TargetGroupArn": "arn:aws:elasticloadbalancing:<REGION>:<ACCOUNT-ID>::targetgroup/TG/DDDD",
                            "Weight": 1
                        }
                    ]
                },
                "TargetGroupArn": "arn:aws:elasticloadbalancing:<REGION>:<ACCOUNT-ID>::targetgroup/TG/DDDD",
                "Type": "forward",
                "Order": 2
            }

In this example, you can see that requests must be authenticated (order 1) before being forwarded (order 2) to the configured target group. You can also see the OnUnauthenticatedRequest configuration, which accepts three options: authenticate (default), allow, and deny.

1. Authenticate: This option should be used for applications that require the user to log in to display the content. When the user is not logged in, the load balancer redirects the request to the Identity Provider’s (IdP) authorization endpoint, and the IdP prompts the user to log in using its user interface.

2. Allow: This option is useful for Single Page Apps (SPAs). It can be used if you need to provide multiple views of the webpage for different audiences such as authenticated and unauthenticated users. For example, if the user is not logged in, and the requests do not contain claims, then the backend can provide a general view of the website. If the user is already logged in, and claims are present in the headers, then the application can provide a personalized view based on those claims.

3. Deny: This option is for use-cases where you have a specific page or path that receives asynchronous requests that reload every few seconds, such as Asynchronous JavaScript and XML (AJAX). In this case, the load balancer returns an HTTP 401 Unauthorized error to calls that have no authentication information instead of redirecting the client to the IdP authorization endpoint. This prevents unauthenticated asynchronous calls to experience an infinite redirect. Note that if the request contains expired authentication information, then ALB redirects the client to the IdP authorization endpoint.

In summary, the authenticate option is suitable for applications that need user authentication to access content, the allow option is helpful for SPAs that need to provide different views based on authentication status, and the deny option is useful for handling asynchronous requests that should not be served if the user is not previously authenticated.

ALB authentication flow

These are the steps ALB uses OpenID Connect (OIDC) to authenticate users:

ALB Authentication flow

Authentication flow

  1. Client sends an HTTPS request to a workload hosted behind an ALB with authentication enabled.
  2. ALB checks the request headers for a session cookie and redirects the user to an IdP authorization endpoint if a session cookie is not present, then the IdP can authenticate the user.
  3. After the user is authenticated, the IdP redirects the user back to the ALB with an authorization grant code.
  4. The load balancer presents the authorization grant code to the IdP token endpoint.
  5. The IdP provides the ID token and access token to the ALB.
  6. The ALB sends the access token to the user info endpoint.
  7. The user info endpoint exchanges the access token for user claims.
  8. The ALB redirects to the original Uniform Resource Identifier (URI) with AWSELBAuthSessionCookie and the user requests the new URI with AWSELBAuthSessionCookie.
  9. The ALB validates the cookie, and it forwards the user info to target with the X-AMZN-OIDC-* HTTP headers set.
  10. The target accepts the request, validates the authentication information, and then sends a response back to the ALB.
  11. The ALB forwards the final response to the client.

All unauthenticated requests goes through steps 1 through 11, while authenticated requests go only from steps 9 through 11, as long as the authentication info has not expired. In other words, after the authentication is completed, the user that sent the correct authentication info ALB gets its requests forwarded to the configured target in the ForwardConfig rule of the target group.

Implementing best practices to secure your target application

In step 10 of the authentication flow, the backend target receives the request from the ALB and provides a response to these requests. Although the ALB only sends requests to the configured targets, your target may potentially receive requests from other sources if its configuration allows this to happen.

Architecture where targets accepting traffic from exclusively from the ALB

Architecture where targets accepting traffic from exclusively from the ALB

To make sure of the security of your application, it is crucial to mitigate the risk of unauthorized access by following these recommended steps:

  1. Restrict ALB targets to receive traffic only from trusted sources:
    • Configure the target’s security groups to accept traffic exclusively from the ALB. This can be accomplished by referencing the security group of the ALB when setting the inbound rules for your target security group. By doing so, you effectively restrict access to your targets, thus making sure that only the ALB can initiate connections to your targets.
    • Deploy ALB targets in private subnets without public IP addresses or Elastic IP addresses. This prevents direct access to the targets from the public internet.
  2. Implement signature validation for the JSON Web Token (JWT) provided in the requests from the ALB, and confirm the signer field from the JWT header matches with the Amazon Resource Name (ARN) of your ALB.

These measures help prevent unauthorized access and maintain the overall security of your application.

The first recommendation is in fact a best practice for any ALB target. It makes sure that your target is not reachable from external agents that could potentially bypass the ALB resources and compromise the security of your application. This way the ALB is wholly responsible for forwarding requests to your targets.

In the following section we provide more details on how to implement signature validation and validation of the signer field.

Implementing JWT validation

When your load balancer successfully authenticates a user, it sends a JWT containing the authentication information through the x-amzn-oidc-data HTTP header to the target. JWT validation is crucial for preventing unauthorized access. ALB forwards JWT-formatted tokens to targets, however it’s important to note that these are not the ID token. Instead, ALB creates a new access token during user authentication and passes only access tokens and user claims to the target. The JWT format used by ALB includes a header, payload, and signature, with the token being base64 URL encoded and padded. ALB uses ES256 (ECDSA using P-256 and SHA256) to generate the JWT signature.

The ALB authentication feature can only be used with HTTPS listeners, making sure of encrypted traffic between the client and the load balancer. To encrypt the authentication information between the ALB and the target, you must configure the target group to use HTTPS. Before enforcing your authorization based on the user claims included in the JWT payload, we strongly recommend your target applications verify the signature of the payload and validate that the signer field contains the expected ALB ARN. The load balancer signs the user claim, using the respective key for the AWS Region, so your application can verify the signature and make sure they were sent by the correct load balancer .

To start the validation of JWT Tokens you must decode the JWT header and later on the JWT payload. When decoding the JWT header, its contents are the JSON object with the following fields:

{
   "alg": "algorithm",
   "kid": "12345678-1234-1234-1234-123456789012",
   "signer": "arn:aws:elasticloadbalancing:region-code:account-id:loadbalancer/app/<YOUR-ALB-NAME>/load-balancer-id", 
   "iss": "url",
   "client": "client-id",
   "exp": "expiration"
}

As you can see, the ALB appends its ARN in the signer field and the client_id that it uses to connect with the configured IDP while creating the JWT headers. The signer field helps you to make sure that that that the headers were signed by the load balancer you configured.

After you have the JWT headers, before decoding the JWT payload, you should verify the payload signature. To do that you need to retrieve the key ID (kid) from the JWT header and use it to look up the public key from the endpoint. The endpoint for each AWS Region is as follows:

https://public-keys.auth.elb.<REGION>.amazonaws.com/key-id

The key uses the PEM encoding format, verifying that the signature helps to make sure that the request hasn’t been tampered. After decoding the JWT payload, its contents are a JSON object that contains the user claims received from the IdP user info endpoint.

{
  "sub": "12345678-1234-1234-1234-123456789012",
  "email": "email@example.org",
  "username": "my_username",
  "exp": 1686153020,
  "iss": "https://<YOUR ISSUER>/xxxxxxxxx"
}

Note that the JWT signature verification can only be properly completed if you have the correct public key. Additionally, to prevent replay of the older signed header, you should also verify the expiration_time to the JWT payload.

Below is a summary of the steps involved in the JWT validation:

  1. The target receives the x-amzn-oidc-data HTTP header.
  2. The base64-encoded JWT header present in the x-amzn-oidc-data HTTP header is decoded.
  3. The “signer” value is validated to ensure it matches the expected ALB ARN configured for this target.
  4. The public key is downloaded from the regional endpoint, using the “kid” (Key ID) value.
  5. The signature is validated on the JWT payload, using the downloaded public key.
  6. The JWT payload is validated to ensure it has not expired, by checking the “exp” (expiration) field.

There are multiple libraries in various programming languages that validate JWT signatures. We provide a sample using the PyJWT library for Python 3.x, with comments at each step to assist you in building upon it.

Signature validation code sample using Python 3.x

import jwt
import requests
import base64
import json

# Configure the region your workload is running e.g. us-east-1
region = region-code

# Configure the expected ALB ARN
expected_alb_arn = 'arn:aws:elasticloadbalancing:region-code:account-id:loadbalancer/app/load-balancer-name/load-balancer-id'

# Store the encoded JWT from the x-amzn-oidc-data HTTP header
encoded_jwt = headers.dict['x-amzn-oidc-data']

# Step 1: decode the JWT header
jwt_headers = encoded_jwt.split('.')[0]
decoded_jwt_headers = base64.b64decode(jwt_headers)
decoded_jwt_headers = decoded_jwt_headers.decode("utf-8")
decoded_json = json.loads(decoded_jwt_headers)
received_alb_arn = decoded_json['signer']

# This compares the ARN you configured in expected_alb_arn with the received_alb_arn.
assert expected_alb_arn == received_alb_arn, "Invalid Signer"
# If the ARN is not the expected one it tells "Invalid Signer"

# Step 2: Extract the key id (kid field) from the decoded JSON from the JWT header
kid = decoded_json['kid']

# Step 3: Retrieve public key from regional endpoint 
url = 'https://public-keys.auth.elb.' + region + '.amazonaws.com/' + kid
req = requests.get(url)
pub_key = req.text

# Step 4: Decode the payload from the encoded JWT by validating the signature
payload = jwt.decode(encoded_jwt, pub_key, algorithms=['ES256'])
# If the signature is invalid, it should return an error, such as:
# Cannot decode JWT token: Signature verification failed

Additional resources

AWS provides a comprehensive set of resources to help you secure your cloud environment and adhere to security best practices. Check these additional resources for more information:

Conclusion

In conclusion, securing your target application when using the ALB authentication feature is critical for maintaining the integrity and security of your application. By implementing the recommended security measures, such as restricting target access exclusively to the ALB and validating the JWT signature and signer field, you can significantly mitigate the risk of unauthorized access. The JWT validation process makes sure that the user claims received by your target application are authentic and originate from the configured ALB. Furthermore, verifying the signer field in the JWT header confirms that the claims were signed by the expected ALB, providing an additional layer of security. By following these best practices, you can enhance the overall security posture of your application and protect it from potential inadvertent access. Adopting a proactive and comprehensive approach to security is essential in today’s ever-evolving threat landscape.

About the authors

Lucas Pellucci Barreto Rolim

Lucas Rolim is a Senior Solutions Architect with Amazon Web Services (AWS), working in the Application Networking team and based in Sydney, Australia. He is passionate about assisting customers in making informed decisions while building on AWS. His primary areas of expertise are Networking and Security.

Luis Felipe Silveira da Silva

Luis Felipe is a Principal Solutions Architect in the ELB Team. He works with a diverse range of load balancing and networking technologies, collaborating with customers and internal teams to design and optimize workloads, along with ensuring successful implementation and adoption of EC2 Networking services.