Lab 03 — OIDC Authorization Transaction Store

Difficulty: 4/5 | Runs locally: yes

Pairs with the Phase 02 WARMUP Chapter 7 (OAuth2/OIDC under the hood) and Chapter 8 (JWT validation as a protocol).

Why This Lab Exists (Purpose & Goal)

"Sign in with Google/Okta/Entra" — the OAuth2 authorization-code flow with OIDC — is how a huge share of the world's applications authenticate, and it is full of subtle, security-critical state. The anti-forgery parameters (state, PKCE, nonce, exact redirect_uri) are not boilerplate; each one defends against a specific, real attack, and getting any of them wrong yields account takeover. The goal of this lab is to implement the transaction state machine that makes the flow safe — binding each login attempt to the browser session that started it, enforcing single-use, and rejecting replay and mix-up.

The Concept, In the Weeds

In the authorization-code flow the app redirects the browser to the identity provider, the user authenticates there (the app never sees the password), and the provider redirects back with a short-lived authorization code the app exchanges — on a back channel — for tokens. Between "start" and "callback," the app must hold per-transaction state, and each element defends an attack:

  • state binds the callback to the browser session that initiated the flow. Without it, an attacker injects their own code into your session — login CSRF — and you end up logged into the attacker's account, leaking everything you do.
  • PKCE (code_verifier / code_challenge) proves the party redeeming the code is the one that started the flow, so a stolen authorization code is useless. Essential for public clients (mobile/SPA) that can't keep a secret.
  • nonce binds the issued ID token to this request, preventing ID-token replay.
  • Exact redirect_uri match — a loose or open redirect lets an attacker steal the code.
  • Single-use, time-bounded — a callback consumed once; a replayed or expired callback fails.

The bug class this prevents is mix-up / replay: reusing an authorization response in a different session, or against a different issuer, or twice. The lab is deliberately a protocol-state exercise, not an identity provider — because the state machine is where the security lives.

Why This Matters for Protecting the Company

Federated login is the front door to the company's applications, and a flaw here is account takeover at scale — often pre-authentication, often affecting every user. These are among the highest-paid bug-bounty findings precisely because the impact is total. When you can implement (and review) the OIDC transaction store correctly — state binding, PKCE, nonce, exact redirect, single-use, expiry — you protect the authentication layer that everything else trusts. You also learn to not make the classic mistake of using an OAuth access token as proof of who the user is (that's the OIDC ID token's job, validated as a protocol).

Build It

Implement the state / nonce / PKCE-verifier lifecycle and single-use callback consumption. Transactions are bound to the browser session, issuer, redirect URI, and expiry; replay and mix-up attempts must fail.

LAB_MODULE=solution pytest -q

Secure Implementation Patterns

The anti-pattern. A callback handler that trusts the redirect without binding it to the transaction that started:

@app.get("/callback")
def callback(code: str, state: str):
    tokens = exchange(code)           # no state check (login CSRF), no single-use, no binding
    login(tokens)                     # attacker injects THEIR code -> you log into attacker's account

The secure pattern — a single-use, session-bound transaction store (mirrors solution.py). The flow stores per-attempt state at start and consumes it exactly once at callback:

class Store:
    def add(self, tx: Transaction) -> None:
        if not all((tx.state, tx.nonce, tx.verifier, tx.session_id, tx.issuer, tx.redirect_uri)):
            raise ValueError("incomplete transaction")          # fail closed on a malformed start
        if tx.state in self.items:
            raise ValueError("duplicate state")                 # state must be unique (CSPRNG)
        self.items[tx.state] = tx

    def consume(self, state, *, session_id, issuer, redirect_uri, now) -> Transaction:
        tx = self.items.pop(state, None)                        # POP = single use; a replay finds nothing
        if tx is None:                 raise ValueError("unknown-or-replayed-state")
        if tx.expires_at <= now:       raise ValueError("expired")
        if (tx.session_id, tx.issuer, tx.redirect_uri) != (session_id, issuer, redirect_uri):
            raise ValueError("transaction-binding-mismatch")     # bind to THIS browser + issuer + redirect
        return tx

What each line defends (Phase 02 WARMUP Ch.7): state uniqueness + session binding = login CSRF; pop (single use) = replay; issuer binding = mix-up across IdPs; redirect_uri binding = code interception; nonce/verifier carried in the tx = ID-token replay and stolen-code reuse (PKCE).

Production practices to carry forward:

  • Generate state, nonce, and the PKCE verifier with a CSPRNG (secrets.token_urlsafe(32)), never random.
  • Validate the ID token as a protocol, not decode(): pin alg, check iss/aud/exp, and match nonce to the stored transaction.
  • Keep transaction TTL short (minutes) and store server-side; the cookie holds only an opaque session id.
  • Use an exact redirect_uri allowlist — no wildcards, no startswith.
  • Treat the access token as an API bearer, not proof of login — identity comes from the verified ID token.

Validation — What You Should Be Able to Do Now

  • Explain, for each of state, PKCE, nonce, and exact-redirect-URI, the specific attack it prevents.
  • Walk the authorization-code + PKCE flow on a whiteboard and identify where each piece of state is created, checked, and consumed.
  • Explain why an access token is not proof of authentication, and what the ID token / aud check is for.
  • Describe a mix-up/replay attack and how single-use + session/issuer binding stops it.

The Broader Perspective

This lab gives you fluency in delegated authentication and authorization, the protocols that bind modern identity together — the same conceptual family as Kerberos tickets (Phase 05), SAML assertions, and workload-identity federation (Phase 07). The recurring theme: a bearer credential is only as safe as the binding that ties it to a session, an audience, a time window, and a single use. Lose any of those bindings and a valid credential becomes a replayable skeleton key. That lens — what is this token bound to, and can it be replayed or redirected? — applies to every token system you will ever review.

Interview Angle

  • "Why does the OAuth code flow use PKCE and state?"state binds the callback to the initiating session (stops login CSRF / code injection); PKCE binds code redemption to the initiator (a stolen code can't be redeemed). With exact redirect-URI matching they stop code interception and injection.
  • "Is an access token proof of who the user is?" — No; it's a bearer authorization for an API. Identity comes from the OIDC ID token (validated: pinned alg, iss, aud, exp, nonce).

Extension (Stretch)

Connect to a local IdP and exercise the full flow with PKCE, nonce, refresh, logout, and signing-key rotation (overlap + emergency revocation).

References

  • Phase 02 WARMUP Chapters 7–8; RFC 6749, RFC 7636 (PKCE), OAuth 2.0 Security BCP (RFC 9700), OpenID Connect Core.