Lakshya

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.

9 min read2 diagramsAll 16 chapters

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.

browseryour appauth serverAPI 1 user clicks sign in2 redirect + code_challenge = S256(verifier)3 GET /authorize?client_id&redirect_uri&state&code_challenge4 user authenticates + consents5 302 back to redirect_uri?code=&state=6 browser delivers the code7 POST /token code + code_verifier + client_secret8 access_token + id_token + refresh_token9 Authorization: Bearer <access_token> WHY PKCE EXISTS (STEP 2 & 7) An attacker who steals the code at step 5 or 6 cannot use it: step 7 requires the verifier, which never left the client. Mandatory in OAuth 2.1 for every client type, not just mobile. WHY state EXISTS (STEP 3 & 6) It binds the callback to the session that started it. Without it, an attacker can feed you their code and silently link your session to their account — login CSRF.
The authorisation code flow with PKCE — the only flow you should be using. The two boxes at the bottom explain the parameters everyone copies without understanding: PKCE defeats code interception, and 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

FlowUse it?Why
Authorization code + PKCEYes — this is the answerWorks for every client type. PKCE is mandatory in OAuth 2.1 for public and confidential clients
Client credentialsYes, for machinesNo user involved. Service-to-service. The right flow for a backend job or an AI agent acting on its own behalf
Device authorizationYes, for input-constrained devicesA TV or CLI shows a code; you authorise on your phone
Refresh tokenYes, with rotationRotate on every use and detect reuse — a replayed refresh token means it was stolen
ImplicitNo. DeprecatedReturned the access token in the URL fragment, exposing it to browser history, referrer headers and any script on the page
Resource owner password credentialsNo. DeprecatedThe 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

THREE TOKENS, THREE AUDIENCES. CONFUSING THEM IS THE MOST COMMON OAUTH BUG. access_token for the API says what you may do life minutes opaque to the client — NEVER parse it the API validates: sig, iss, aud, exp, scope treat as a bearer credential id_token for the CLIENT says who the user is life one use a JWT you DO parse — that is its job validate nonce, iss, aud, exp, signature never send it to an API refresh_token for the auth server says give me new tokens life days–months the most valuable of the three rotate on use; detect reuse if stolen, it is a persistent session THE FOUR VALIDATION CHECKS AN API MUST DO ON EVERY REQUEST — SKIPPING ANY ONE IS A FULL BYPASS 1 signatureagainst the JWKS, alg pinned2 issyour issuer, string compare3 audthis API, not another one4 exp / nbfwith minimal clock skew The classic failure: verifying the signature but not aud. A valid token for a different service in the same tenant is then accepted —a lateral-movement primitive. And never trust the token's own alg header. “alg”: “none” and RS256→HS256 confusion are both live bugs in the wild.
Three tokens, three audiences. The most common OAuth bug in production is sending an 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_token is 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: none and RS256-to-HS256 confusion attacks work, where an attacker signs a token using your public key as an HMAC secret.
  • Check iss exactly, as a string comparison against your expected issuer. Not a prefix match, not a contains check.
  • Check aud is 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 exp and nbf with 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 nonce against 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

What is the difference between OAuth 2.0 and OpenID Connect?OAuth authorises access to a resource. OIDC adds authentication — an id_token with standard claims about the user. Using raw OAuth for login is the classic mistake.
Why is PKCE now recommended for confidential clients too?Because it protects the code exchange even if the client secret has leaked, and secrets leak. It defends a different thing than the secret does.
Which JWT validation check is most often skipped, and what does skipping it cost?The audience claim. Without it, a token minted for any other service in the same tenant is accepted, which is a lateral movement primitive.
Why must redirect URIs be exact-matched?Prefix or wildcard matching lets an attacker register a URI that passes the check and receives the authorisation code. It is the most common OAuth implementation flaw.
← Zero trust, honestlySAML and enterprise federation →