Production engineering
Identity as its own domain
OAuth 2.0 and OpenID Connect in production — Authorization Code with PKCE, JWT validation against JWKS, and SSO between applications.
- OAuth 2.0
- OpenID Connect
- PKCE
- JWT
- RS256
- JWKS
- SSO
- Java
Context
Authentication is the one part of an application where a plausible-looking implementation and a correct one are hard to tell apart from the outside. Both let the right user in. Only one of them keeps the wrong user out. Working on identity and access management is what turned OAuth from a set of terms I could define into a set of decisions I could defend — and taught me that most of the difficulty is not the cryptography but the trust boundaries.
Written at the level a public case study can go: the shape of the problems and how I reasoned about them. No proprietary code, internal endpoints, schemas, customer details or unreleased work appears here.
Identity as a separate domain
The boundary in the middle is the point. Everything above it issues assertions; everything below it verifies them and decides what they permit.
The flow, step by step
This is the diagram I wish someone had shown me first. Open any step for the reasoning behind it.
Authorization Code flow with PKCE
Step through it. The colour tells you whether a message can be seen by an attacker who controls the browser.
Nothing security-relevant has happened yet. Worth stating, because the flow is often explained as if it starts at the identity provider — it does not.
Local: happens inside one participant
The application is about to make two decisions: where to send the user, and what secret to bind this particular login attempt to.
Front channel: via the browser — treat as observable
The verifier is the secret PKCE is built on — it never leaves the application until the final token call. state defends against login CSRF; nonce binds the eventual ID token back to this request.
Local: happens inside one participant
This is the step people find surprising: the authorization server cannot “open a page” for the user. It has no channel to the browser. The only thing the application can do is reply with a 302 and let the browser choose to follow it.
Front channel: via the browser — treat as observable
The domain change is the point. The user is typing their password into the identity provider, not into the application — so the application never sees the credential at all.
Front channel: via the browser — treat as observable
If a session already exists here, the user sees nothing and is redirected straight back. That silent case is exactly what single sign-on is.
Local: happens inside one participant
The code travels through the browser, so assume it is observable — in history, in logs, in a referer header. That is precisely why it is not a token, is single-use, and expires in about a minute.
Front channel: via the browser — treat as observable
A state mismatch means this callback did not originate from a login this session started — that is login CSRF, and the request has to be rejected before the code is used.
Front channel: via the browser — treat as observable
The back channel. An attacker who stole the code from the browser cannot complete this call: they would also need the code_verifier, which never travelled through the browser. This is the whole of PKCE.
Back channel: direct server-to-server — not observable by the browser
Three tokens with three jobs: the access token authorizes API calls, the ID token states who signed in, and the refresh token buys a new access token without sending the user through login again.
Back channel: direct server-to-server — not observable by the browser
Public key distribution is what makes key rotation possible without redeploying every service. RS256 is asymmetric: the identity provider signs with a private key nobody else holds, and anyone can verify.
Back channel: direct server-to-server — not observable by the browser
A valid signature only proves the identity provider issued this token — not that it was issued for you. A token minted for a different audience will verify cryptographically and must still be rejected.
Local: happens inside one participant
Login is finished, and the browser still holds no token — only a session cookie. Keeping tokens server-side is what stops a cross-site scripting bug from becoming a token theft.
Front channel: via the browser — treat as observable
The engineering
Why the authorization code exists at all
The first thing worth understanding is that the authorization server cannot talk to the application directly — it can only redirect the browser. Everything in the Authorization Code flow follows from that: the code is a short-lived, single-use reference handed through an untrusted channel (the URL bar, the history, the referer header), which the client then exchanges for tokens over a direct back-channel call that an attacker cannot observe. Tokens never travel through the browser’s address bar.
PKCE, and the attack it actually stops
PKCE exists because a redirect URI is not a secret. If a malicious application on the same device can register or intercept the redirect, it can steal the authorization code in transit. PKCE binds the code to the client that started the flow: the client sends a hash of a random verifier up front, and must present the original verifier at the token endpoint. Intercepting the code is then useless without the verifier, which never left the legitimate client.
Validating a token means more than checking the signature
An RS256 JWT is signed, not encrypted — anyone can read the claims; only the holder of the private key could have produced the signature. Verification means fetching the right public key from the JWKS endpoint by key ID, checking the signature, and then checking the claims that actually scope the token: issuer, audience, expiry, and nonce where the flow requires it. A token that verifies cryptographically but was issued for a different audience is not a valid token for you.
Authentication and authorization are different questions
Authentication answers "who is this", authorization answers "may they do this", and conflating them produces systems where being logged in is accidentally the same as being permitted. Keeping identity as its own domain with its own boundary — the identity provider issues assertions, the application decides what those assertions entitle you to — is what makes permissions reasonable to change later.