Identity & Access · Chapter 9 of 16
OAuth 2.0 and OIDC, properly
The protocol behind every ‘sign in with’ button. Four token types, one flow that matters, and the validation step whose absence is a complete authentication bypass.
OAuth and OpenID Connect are the most widely deployed and most widely misunderstood protocols in identity. The confusion has a specific root and clearing it up fixes most of the rest: OAuth 2.0 is an authorisation framework, not an authentication protocol. It answers “may this application access that resource on the user's behalf?” It does not, on its own, tell the application who the user is.
OpenID Connect is a thin layer on top of OAuth that adds exactly that: an id_token and a standard set of claims about the user. Every “sign in with Google” button you have ever pressed is OIDC, and the many implementations that used raw OAuth for login before OIDC existed were subtly and sometimes seriously broken.
state defeats login CSRF.The four roles, named precisely
- Resource owner — the human. The person whose data it is.
- Client — the application asking for access. Confidential if it can keep a secret (a server); public if it cannot (a single-page app, a mobile app).
- Authorization server — issues tokens. Entra, Okta, Auth0, Keycloak, Cognito.
- Resource server — the API holding the data, which validates the token and enforces scope.
The flows, and which ones are dead
| Flow | Use it? | Why |
|---|---|---|
| Authorization code + PKCE | Yes — this is the answer | Works for every client type. PKCE is mandatory in OAuth 2.1 for public and confidential clients |
| Client credentials | Yes, for machines | No user involved. Service-to-service. The right flow for a backend job or an AI agent acting on its own behalf |
| Device authorization | Yes, for input-constrained devices | A TV or CLI shows a code; you authorise on your phone |
| Refresh token | Yes, with rotation | Rotate on every use and detect reuse — a replayed refresh token means it was stolen |
| Implicit | No. Deprecated | Returned the access token in the URL fragment, exposing it to browser history, referrer headers and any script on the page |
| Resource owner password credentials | No. Deprecated | The app collects the user's actual password, which defeats the entire purpose and makes MFA and federation impossible |
PKCE, explained in one paragraph so you never have to look it up again
Before starting, the client generates a random verifier and sends only its SHA-256 hash — the challenge — to the authorisation server. When it later exchanges the authorisation code for tokens, it must present the original verifier.
An attacker who intercepts the authorisation code — through a malicious app registered for the same custom URL scheme, a leaky referrer, or a compromised redirect — cannot use it, because they do not have the verifier and cannot derive it from the hash.
That is the whole idea. It was originally designed for mobile apps and is now recommended for everything, because ‘the client secret protects us’ assumes the secret has not leaked, and secrets leak.
The tokens, and the confusion that causes real bugs
id_token to an API, or parsing an access_token in the client. Both are category errors and both are extremely common.Two rules that prevent most of the errors here:
- An access token is opaque to the client. Even if it happens to be a JWT — a JSON Web Token, pronounced ‘jot’: a few facts about someone, packed into a string and signed, so a receiver can check it without calling anyone — that you could decode, you must not. It is addressed to the API, its format may change without notice, and any logic you build on its contents will break. If the client needs to know who the user is, that is what the
id_tokenis for. - An id_token is never sent to an API. It is proof of authentication delivered to the client, with an audience claim naming the client. An API that accepts it is accepting a token issued for somebody else's purpose.
Validating a JSON Web Token — the part that is a security boundary
If you are building the resource server, this is the code that matters, and every one of these checks has been the subject of a real CVE somewhere:
- Verify the signature against the issuer's published JWKS, fetched over TLS and cached with respect to key rotation. Pin the expected algorithm. Never read the algorithm from the token's own header — that is how
alg: noneand RS256-to-HS256 confusion attacks work, where an attacker signs a token using your public key as an HMAC secret. - Check
issexactly, as a string comparison against your expected issuer. Not a prefix match, not a contains check. - Check
audis this API. This is the check most often omitted, and omitting it means a valid token for any other service in the same tenant is accepted by yours — a ready-made lateral movement primitive. - Check
expandnbfwith minimal clock skew tolerance. Sixty seconds, not an hour. - Check the scopes actually authorise the operation being performed. A valid token is not an authorisation decision; it is an input to one.
- For an id_token, also check
nonceagainst the value you sent, which binds the token to your authentication request.
Common vulnerabilities, and what each one costs
Open redirect on redirect_uri. If the authorisation server accepts a redirect URI by prefix or wildcard match, an attacker registers https://yourapp.com.evil.com or exploits a path traversal and receives the code. Redirect URIs must be exact-matched against a registered allowlist. This is the single most common OAuth implementation flaw.
Missing state. Without it, an attacker can initiate a flow, capture their own authorisation code, and feed it to your callback while you are logged in. Your session is then silently bound to their account — and everything you subsequently do is visible to them. It is called login CSRF and it is subtle enough that people dismiss it as harmless, which it is not.
Token leakage through the referrer. Any token in a URL can leak via the Referer header, browser history, server logs and analytics. This is the main reason the implicit flow was deprecated.
Over-broad scopes. Requesting full mailbox access when you need to send one message. Users approve it, and now a compromise of your application is a compromise of their mail. Scope minimisation is an application-security decision, not a UX one.
Refresh tokens without rotation. A stolen non-rotating refresh token is a permanent session. With rotation and reuse detection, a replay reveals the theft and lets you revoke the whole family.
What is changing: OAuth 2.1 and agent identity
OAuth 2.1 consolidates a decade of practice: PKCE required everywhere, implicit and password grants removed, exact-match redirect URIs, and refresh token rotation for public clients. If you describe an OAuth design in an interview, describing it as 2.1 with those defaults is the answer that lands.
The live frontier is agent and workload identity. An AI agent acting for a user needs to act with less authority than that user, for a bounded time, with an audit trail that distinguishes the agent's actions from the human's. The building blocks exist — token exchange for delegation, the client credentials flow for autonomous action, short-lived tokens with narrow scopes, and workload identity federation so a workload proves what it is rather than holding a secret — and the practice is not yet settled. Being able to say that clearly, including what is unsolved, is worth more than claiming a confident answer.
Check yourself