Learning outcomes
- Trace the authorization code flow across its four protocol roles.
- Generate and bind an S256 PKCE challenge to one transaction.
- Validate state, issuer, redirect URI, code, and token response.
- Test code theft, injection, replay, downgrade, and mix-up failures.
Protocol roles
The resource owner is the person granting access. The client requests authorization and receives tokens. The authorization server authenticates the person, records authorization, issues the one-time code, and exchanges it for tokens. The resource server accepts a suitable access token for a protected resource.
The browser is a message carrier, not a trusted protocol role. A code that travels through the browser can leak or be injected. PKCE binds that code to a verifier held by the client instance that started the transaction. RFC 9700 Section 2.1.1 requires public clients to use PKCE and recommends it for confidential clients. S256 is the method that does not expose the verifier in the authorization request.
Message flow
- The client generates an unpredictable
statevalue and a high-entropycode_verifier. It computescode_challenge = BASE64URL(SHA256(code_verifier))without padding. - The client stores the verifier with the expected issuer, redirect URI, browser transaction, expiry, and state. It sends the browser to the authorization endpoint with the client identifier, exact registered redirect URI, requested resource or scopes,
state,code_challenge, andcode_challenge_method=S256. - The authorization server authenticates the resource owner and obtains authorization. It binds a short-lived, single-use authorization code to the client, redirect URI, approved request, and PKCE challenge.
- The authorization server redirects the browser to the exact client callback with the code, state, and an issuer identifier when RFC 9207 is used.
- The client matches and consumes state, checks the issuer, and sends the code, exact redirect URI, client authentication when required, and original verifier to the token endpoint.
- The authorization server verifies the code, client binding, redirect URI, expiry, one-time status, and
S256challenge. It consumes the code atomically and returns tokens. - The client validates the token response and sends the access token only to its intended resource server.
Validation and failure cases
Reject these transactions:
- The callback state is missing, mismatched, expired, or already consumed.
- The response issuer differs from the issuer bound to the transaction.
- The redirect URI differs by scheme, host, port, path, or other registered component.
- The code was issued to another client, redirect URI, or browser transaction.
- The verifier does not match the stored S256 challenge.
- A token request includes a verifier for an authorization request that had no challenge. This prevents a PKCE downgrade.
- The code is expired or already used. Two simultaneous redemptions must not both succeed.
- The token issuer or audience is wrong, or the client sends the access token to another resource.
Test code injection by starting two browser transactions and swapping the returned codes. Test code theft by redeeming the intercepted code without the verifier. Test mix-up with two authorization servers and one callback. Test an open redirect and a redirect URI with a near-match host or path.
Design tradeoffs and residual risk
PKCE protects the code. It does not authenticate the authorization server, validate the callback issuer, protect a token after issuance, or replace client authentication for a confidential client. State can bind browser context and prevent request forgery, but state alone does not solve authorization-server mix-up.
Exact redirect registration reduces ambiguity but makes deployment changes more deliberate. Shared callbacks simplify application code but require strong issuer and transaction binding. Long transaction windows help users complete authentication but increase replay and stale-state exposure.
Residual risk includes compromised client code, browser script injection, malicious authorization endpoints, token leakage after exchange, and weak resource-server validation. Keep the code and state out of logs and remove callback query values from browser history where the client architecture permits it.
Pomerium boundary
Pomerium performs its configured identity-provider authentication flow and manages the matching callback and session. Operators configure trusted identity providers and redirect destinations. An upstream application that implements a separate OAuth client owns its PKCE, state, issuer, redirect, and token validation.
Exercise
Capture a test flow without recording secrets. Build a transaction table with issuer, client, redirect URI, state hash, challenge method, code creation and consumption time, requested resource, token audience, and final result.
Run six negative cases: swapped code, missing verifier, wrong verifier, downgrade attempt, second code redemption, and response from a different issuer. Each case must fail before a session or resource request is accepted.
Evaluation checklist
- Does every transaction use a unique high-entropy verifier and
S256challenge? - Is the verifier stored with the expected issuer, browser transaction, redirect URI, and expiry?
- Does the client reject wrong state, issuer, redirect, code binding, and token audience?
- Are authorization codes short-lived, single-use, and atomically consumed?
- Can code theft, injection, downgrade, replay, and mix-up tests all fail safely?
Next learning unit
OpenID Connect Request Correlation
Bind an OpenID Connect response to the initiating browser, issuer, client, redirect URI, nonce, and authorization request.
