An internal admin console for the platform team starts as a Flask app behind an ALB, with a single hardcoded credential in an environment variable. Three months later there are 12 people on the team, three of whom have left and one of whom rotated to a different role. The credential has not changed. The path of least resistance to “real auth” looks like building a login page in Flask, integrating with the company’s Okta tenant, managing tokens, handling refresh. A week of work for an internal tool. The shorter path is ALB authentication — the ALB does the Okta dance, validates the user, and forwards the request to the Flask app with a signed JWT in a header. The Flask app trusts the header and reads the user from the JWT. No login page, no token management, no refresh logic. The pattern works for Cognito user pools (AWS-native), OIDC providers (Okta, Auth0, Google Workspace, Azure AD), and is the right default for internal tools, admin consoles, and any application where you would otherwise reach for a reverse proxy with oauth2-proxy in front.
What’s happening
ALB supports two authentication actions on a listener rule:
-
authenticate-cognito— ALB redirects unauthenticated requests to a Cognito user pool hosted UI, completes the OAuth flow, sets a session cookie (AWSELBAuthSessionCookie), and forwards the request to the target with a signed JWT in theX-Amzn-Oidc-Dataheader. The target reads the header to identify the user. -
authenticate-oidc— Same flow, but against an OIDC-compliant identity provider (Okta, Auth0, Google Workspace, Azure AD, Keycloak, etc.). You configure the issuer URL, the client ID and secret, the authorization endpoint, the token endpoint, and the user-info endpoint. ALB handles the redirect, code exchange, token validation, and session cookie.
The flow:
- Client requests
/admin/dashboard. - ALB checks for the session cookie. None present.
- ALB redirects the client to the identity provider’s authorization endpoint (e.g.
https://example.okta.com/oauth2/v1/authorize) withclient_id,redirect_uri,response_type=code,scope. - User authenticates at the IdP (MFA, etc.).
- IdP redirects back to ALB’s
/oauth2/idpresponseendpoint with an authorization code. - ALB exchanges the code for tokens at the IdP’s token endpoint.
- ALB validates the ID token, fetches user info, sets the session cookie, and redirects the client to the original URL.
- Now the client has the cookie. ALB forwards the request to the target, with
X-Amzn-Oidc-Data(the signed JWT containing user claims),X-Amzn-Oidc-AccessToken, andX-Amzn-Oidc-Identityheaders.
The target trusts the headers because they were set by ALB after the auth flow. The JWT in X-Amzn-Oidc-Data is signed with an AWS-managed key; the target can verify the signature against the JWKS endpoint at https://public-keys.auth.elb.<region>.amazonaws.com/<kid>. For internal tools where ALB is the only ingress and the security group enforces that, signature verification can be skipped and the header trusted directly.
What this saves you:
- No login UI in your application. ALB redirects to the IdP; the IdP handles the UI.
- No token storage in the application. The session cookie is opaque to the target; the JWT in the header is read-only.
- No refresh logic. ALB refreshes the token when the session cookie is renewed.
- Per-rule auth. A single ALB can host an authenticated
/adminand an unauthenticated/healthzbecause each is a separate listener rule with its own actions.
What this does not solve:
-
Authorization. ALB authentication confirms who the user is. It does not say what they can do. The application still needs to read the user’s groups or roles from the JWT and decide whether to allow the action. ALB does have an “on-unauthenticated-request” parameter but it controls the redirect-vs-deny behavior, not fine-grained access.
-
Service-to-service auth. ALB authentication is for human-facing flows. A machine client cannot complete the OAuth code flow. For service-to-service, use mTLS, signed requests, or a separate API key path.
-
Cross-region SSO state. The session cookie is per-ALB. A user authenticated against one ALB is not automatically authenticated against another in a different region. For SSO across multiple ALBs, configure the same OIDC client and the IdP handles the session.
How to verify
Configuring a listener rule for Cognito auth:
aws elbv2 modify-listener \
--listener-arn "$LISTENER_ARN" \
--default-actions '[
{
"Type": "authenticate-cognito",
"AuthenticateCognitoConfig": {
"UserPoolArn": "arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_abc",
"UserPoolClientId": "1a2b3c4d5e6f",
"UserPoolDomain": "admin-tools-auth"
},
"Order": 1
},
{
"Type": "forward",
"TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:...",
"Order": 2
}
]'
The two actions are sequential — Cognito first, then forward. ALB will not forward to the target until the Cognito action completes.
Configuring an OIDC rule for Okta:
aws elbv2 create-rule \
--listener-arn "$LISTENER_ARN" \
--priority 10 \
--conditions '[{"Field":"path-pattern","Values":["/admin/*"]}]' \
--actions '[
{
"Type": "authenticate-oidc",
"AuthenticateOidcConfig": {
"Issuer": "https://example.okta.com",
"AuthorizationEndpoint": "https://example.okta.com/oauth2/v1/authorize",
"TokenEndpoint": "https://example.okta.com/oauth2/v1/token",
"UserInfoEndpoint": "https://example.okta.com/oauth2/v1/userinfo",
"ClientId": "0oa1b2c3d4e5f6g7h8i9",
"ClientSecret": "<secret>",
"Scope": "openid profile email groups",
"OnUnauthenticatedRequest": "authenticate"
},
"Order": 1
},
{
"Type": "forward",
"TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:...",
"Order": 2
}
]'
The Scope is what determines what claims arrive in the JWT. openid profile email groups is the standard combination for “tell me who they are and what groups they belong to.”
The Okta application configuration on the IdP side must include:
- Redirect URI:
https://<your-alb-domain>/oauth2/idpresponse— this is fixed by ALB. - Grant type: Authorization Code.
- Scopes: Whatever you list in the ALB rule’s
Scopeparameter.
Verifying the target receives the headers after the flow completes. A test endpoint that dumps headers:
# minimal Flask handler
@app.route('/whoami')
def whoami():
import json, base64
oidc_data = request.headers.get('X-Amzn-Oidc-Data', '')
# JWT is base64-encoded; split on dots and decode the payload
parts = oidc_data.split('.')
if len(parts) == 3:
payload = base64.urlsafe_b64decode(parts[1] + '==')
return json.dumps(json.loads(payload), indent=2)
return 'no oidc data'
After authenticating, the response contains the JWT claims — email, groups, sub, iss, exp. The application reads these directly.
Confirming the session cookie is set:
curl -v -c cookies.txt -L https://admin.example.com/
# follow the redirects through the IdP, then check the cookie jar
grep AWSELBAuthSessionCookie cookies.txt
The cookie is path /, domain the ALB domain, HTTP-only, secure.
The fix
The deployment runbook for adding ALB auth to an existing internal tool:
-
Choose the provider. Cognito if the user directory is AWS-native or you do not have an existing IdP. OIDC against the company IdP (Okta, Azure AD, Google Workspace) if the directory already lives there.
-
Create the IdP application. Issuer URL, client ID and secret, redirect URI set to
https://<alb-domain>/oauth2/idpresponse. Scopes set toopenid profile emailplus whatever group claim you need for authorization. -
Decide on per-rule scope. A single ALB with
/admin/*authenticated and/api/healthunauthenticated lets the health check work without authentication. The default action covers unspecified paths. -
Configure the listener rules with both authenticate and forward actions in order. The auth action is
Order: 1, the forward isOrder: 2. -
Update the application to read the JWT header. Add a middleware that parses
X-Amzn-Oidc-Data, extracts the email and groups, and attaches them to the request. The application is now identity-aware without owning the auth flow. -
Test the unhealthy-path. The IdP application’s redirect URI must match exactly. A trailing slash difference fails silently with a generic error. Verify with the IdP’s logs.
-
Decide on session duration. The ALB session cookie defaults to 7 days. For an admin console, 8 hours is more appropriate. Set
SessionTimeouton the auth action.
Three rules we hold:
Verify the JWT signature in production-grade apps. For internal tools where ALB is the only ingress and the security group enforces that, trusting the header is acceptable. For anything that could be reached by another route, fetch the JWKS and verify the signature. AWS publishes the keys at the documented endpoint.
Skip ALB auth for service-to-service. Machine clients cannot complete the redirect flow. Use a separate listener rule for /api/* with an authentication scheme appropriate for machines (API key, mTLS, signed requests).
The auth action runs before any other action on the rule. If you also want to log unauthenticated requests, instrument the IdP, not the target. The target only sees requests that already authenticated.
For more on the listener rule mechanics this builds on, see ALB listener rules — host and path routing. For health checks that need to remain unauthenticated, see Health check tuning for SaaS targets.
Stack Harbor designs ALB + NLB topologies and the operating model around them as part of Managed AWS onboarding and ongoing Managed Cloud Ops. If your internal tools still share a hardcoded credential, start with an infrastructure consult.