DEV Community

Cover image for 🔥 We Deleted Our Login Code: ALB OIDC for Serverless Frontends
Suraj Khaitan
Suraj Khaitan

Posted on

🔥 We Deleted Our Login Code: ALB OIDC for Serverless Frontends

How moving auth to the load balancer with ALB’s authenticate_oidc made our UI simpler, our defaults safer, and our incidents rarer


The Day “Just Store the Token” Stopped Being Funny

At some point, every frontend team gets the same suggestion:

“Just do OAuth in the browser, store the token, and attach it on API calls.”

It works—until it doesn’t.

Because the moment your UI becomes responsible for token storage, refresh logic, callback routes, and logout semantics, your “frontend” quietly turns into an auth product.

We fixed this by doing something that feels almost illegal:

We let the load balancer handle the login.

Specifically: AWS Application Load Balancer (ALB) + authenticate_oidc + a serverless frontend target (Lambda).


TL;DR (If You Only Read One Section)

  • Problem: App-level OIDC spreads secrets + token handling across every UI route and runtime.
  • Move: Put OIDC at the edge using ALB authenticate_oidc.
  • Result: Less auth code in the app, fewer token footguns, and a “secure-by-default” perimeter.
  • Tradeoff: Local dev + logout semantics require intentional design.

Why This Pattern Is Trending Right Now

Across dev communities lately, the popular themes are consistent:

  • “Stop overbuilding auth in every app.”
  • “Move concerns up the stack.”
  • “Make security the default, not a checklist item.”

Edge-auth patterns (ALB OIDC, gateway authorizers, access proxies) are having a moment because they reduce the number of places a team can accidentally get auth wrong.


The Real Problem: Token Chaos Isn’t One Bug—It’s a Lifestyle

If you do OIDC inside the frontend, you almost inevitably accumulate:

  • A callback route you must never break
  • Token storage debates (localStorage vs memory vs cookies)
  • Refresh token logic (and the day it fails in production)
  • “Why did it log me out?” issues
  • Security reviews that keep expanding scope

And the nastiest part is: it’s not one critical bug—it’s a hundred tiny sharp edges.


The Pivot: Authentication at the ALB

When you use authenticate_oidc, the ALB becomes the bouncer:

  • Unauthenticated requests get redirected to your Identity Provider (IdP)
  • The ALB completes the OIDC flow
  • The ALB maintains an authenticated session (cookie-based)
  • Only authenticated requests reach your target

Your serverless frontend (often a Lambda router / SSR / fallback handler) simply… serves pages.

The vibe shifts from:

“Did we implement OAuth correctly?”

to:

“If I got a 200, I’m logged in.”


The Request Flow in 30 Seconds

Browser
  |
  | GET /anything
  v
ALB (authenticate_oidc)
  |
  | not logged in?
  | 302 -> IdP
  v
IdP (login)
  |
  | 302 -> ALB callback
  v
ALB (sets session cookies)
  |
  | forward
  v
Lambda target (serverless frontend router)
Enter fullscreen mode Exit fullscreen mode

Notice what’s missing:

  • No client-side token parsing
  • No callback handler in your React app
  • No refresh logic scattered across fetch calls

A Minimal, Anonymized CDK Snippet

This is intentionally “shape only” (no real URLs, no org names). The essence is:

1) forward to a Lambda target group
2) wrap it with authenticate_oidc

from aws_cdk import aws_elasticloadbalancingv2 as elbv2
from aws_cdk import aws_elasticloadbalancingv2_targets as targets
from aws_cdk import SecretValue

frontend_tg = elbv2.ApplicationTargetGroup(
    scope,
    "FrontendTg",
    target_type=elbv2.TargetType.LAMBDA,
    targets=[targets.LambdaTarget(frontend_router_lambda)],
)

listener.add_action(
    "FrontendWithOidc",
    priority=100,
    conditions=[elbv2.ListenerCondition.path_patterns(["/*"])],
    action=elbv2.ListenerAction.authenticate_oidc(
        issuer="https://idp.example/",
        authorization_endpoint="https://idp.example/oauth2/authorize",
        token_endpoint="https://idp.example/oauth2/token",
        user_info_endpoint="https://idp.example/oauth2/userinfo",
        client_id="<client-id>",
        client_secret=SecretValue.secrets_manager("/path/to/oidc-secret"),
        next=elbv2.ListenerAction.forward([frontend_tg]),
    ),
)
Enter fullscreen mode Exit fullscreen mode

Quick rules that save pain:

  • Keep the OIDC secret in a secret manager, not env vars.
  • Make sure listener priorities don’t collide.
  • Default to protecting /* unless you truly want public routes.

How This Changed Our Security Posture (In Plain English)

1) “Secure by default” stops being a slogan

With ALB OIDC, every path behind the listener rule becomes authenticated by default. You’re no longer relying on every route guard, every component, and every refactor to “remember auth.”

2) Less token exposure in the browser

The browser is a hostile environment. Reducing token handling in the UI reduces your exposure to:

  • XSS turning into token theft
  • accidental logging of sensitive values
  • copy-paste auth bugs across micro-frontends

3) Fewer app secrets

If your frontend app doesn’t need to “be an OAuth client,” it also needs fewer secrets and fewer complicated deployment rules.


The Subtle but Important Split: Auth vs Authorization

ALB OIDC is excellent at authentication (“who are you?”).

But you still need strong authorization (“what can you do?”):

  • RBAC: role-based permissions
  • ABAC: tenant/env/resource scoping

The clean division:

  • ALB: verify the user is logged in
  • Backend: enforce permissions and data scope

If you try to do all authorization at the load balancer, you’ll end up with something brittle and hard to evolve.


Gotchas (A.K.A. The Part Everyone Learns in Production)

1) Callback path behavior

ALB uses a callback endpoint (often something like /oauth2/idpresponse). Make sure your routing rules don’t accidentally break it.

2) Claims can get huge

Too many groups/roles/claims can hit header/cookie limits. Mitigations:

  • keep tokens/claims lean
  • fetch richer profile data server-side
  • store heavy identity in your own session store

3) Logout is three separate things

There’s:

  • app logout
  • ALB session cookie
  • IdP session

Define what “Logout” means for your UX and compliance requirements.

4) Local dev can feel weird

Production has ALB OIDC; your laptop doesn’t.

Good local-dev patterns:

  • inject mocked identity headers in dev
  • run a lightweight local gateway that simulates “auth at the edge”
  • keep backend authorization testable without a real IdP

A Practical Rollout Checklist

  • Verify OIDC endpoints: issuer + authorize + token + userinfo
  • Store the client secret in a secret manager
  • Confirm listener rule priority ordering
  • Ensure callback path is reachable through routing rules
  • Enforce HTTPS everywhere
  • Enable ALB access logs
  • Document logout behavior (what it clears)
  • Write down the local-dev story (seriously)

When You Should Not Use ALB OIDC

Avoid / reconsider if:

  • you need complex per-request authorization decisions before forwarding
  • you don’t have an ALB in the request path (pure CDN with no origin auth)
  • your org mandates a different gateway or zero-trust access layer

Closing: Make the Safe Path the Easy Path

The benefit of this pattern isn’t novelty.

It’s that you can remove an entire category of mistakes:

  • less auth code in the UI
  • fewer ways to leak tokens
  • consistent enforcement across routes

And when security is the default, teams move faster—because fewer changes require “special auth handling.”


If you’ve done edge auth (ALB OIDC, gateway authorizers, access proxies), what hurt most for you: local dev, logout, or claim size?


Resources

  • AWS Docs: Application Load Balancer authentication actions (OIDC)
  • AWS CDK: ListenerAction.authenticate_oidc
  • OAuth 2.0 / OIDC basics (for understanding redirects, authorization code flow)

About the Author

Suraj Khaitan — Gen AI Architect | Building scalable platforms and secure cloud-native systems

Connect on LinkedIn | Follow for more engineering and architecture write-ups


Top comments (0)