OAuth2, OIDC and JWT demystified
The three most confused acronyms in authentication, what each one solves, and the mistakes that show up in almost every codebase.
The confusion starts with the name. OAuth2 has "auth" in it and is not about login. OIDC is about login and almost nobody knows its full name. And JWT is neither: it is a token format that both of them use.
Let us take them one at a time.
Authentication is not authorisation
Authentication (authn) answers: who are you?
Authorisation (authz) answers: what are you allowed to do?
They are different things, solved by different mechanisms, and the classic mistake is doing the first well and forgetting the second on some endpoint, which is exactly the IDOR from the previous article.
OAuth2: delegated authorisation
The problem OAuth2 solves: giving an application limited access to a resource of yours without handing over your password.
You want the calendar app to read your Google Calendar. Before OAuth, the solution was giving the app your Google password, which gave it access to everything, forever, with no way to revoke.
With OAuth2, you get redirected to Google, authenticate there, authorise specific scopes ("read calendar"), and the app receives a limited, revocable access token.
The actors: the resource owner (you), the client (the app), the authorisation server (Google) and the resource server (the Calendar API).
The flow you should use today: Authorization Code with PKCE. For every kind of client: web, mobile, SPA.
PKCE exists to solve a specific attack: on mobile, another app could intercept the authorisation code during the redirect. With PKCE, the client generates a random secret, sends its hash at the start and the original value at the end. Whoever intercepted the code does not have the secret.
The flows you should no longer use: implicit (returns the token straight in the URL, which leaks into history and logs) and password grant (the app receives the password, which defeats the purpose).
OIDC: login on top of OAuth2
OpenID Connect is a thin layer over OAuth2 that adds one thing: the identity token.
While the access token says "the bearer of this token may call these APIs", the identity token says "this person is so-and-so, authenticated at this time, by this method".
That is why "Sign in with Google" is OIDC, not plain OAuth2. And it is why using plain OAuth2 for login is a known mistake: the access token was not designed to prove identity, and using it that way opens the door to the confused deputy problem.
OIDC also standardises the discovery endpoint (/.well-known/openid-configuration), which makes
integration almost automatic in modern libraries.
JWT: the format
Three parts separated by dots: header, claims and signature, each in base64url.
The fundamental misunderstanding: a JWT is signed, not encrypted. Anybody can read the contents. Paste one into a decoder and look.
Never put sensitive data in there.
What the signature guarantees is integrity: nobody altered the contents. And authenticity: it was issued by whoever holds the key.
- OAuth2delegated authorisationGive an app limited access to your resource without handing over the password. It is not login.
- OIDCloginA thin layer over OAuth2 that adds the identity token. It is what sits behind Sign in with Google.
- JWTformatSigned, not encrypted: anybody can read it. It guarantees integrity and authenticity, not secrecy.
The four JWT mistakes
1. Trusting the algorithm that comes in the token.
The header says which algorithm was used. If your library trusts that, an attacker sends
"alg": "none" and forges any token they like. Or, worse, swaps RS256 for HS256 and uses the public
key (which is public) as the HMAC secret.
Fix: pin the expected algorithm on the server. Never read it from the token.
2. Not validating aud and iss.
A perfectly valid token, issued by a legitimate provider, for another service, must not be valid on yours. If you only verify the signature, it is.
3. Expiry that is far too long, or absent.
An access token should last minutes. Renewing is the refresh token's job, with different storage and rotation.
An access token valid for thirty days is a permanent credential circulating through logs, proxy caches and browser history.
4. Believing you can revoke it.
You cannot. A JWT is valid until it expires: that is its nature, and it is the reason it scales without consulting the issuer.
If you need immediate revocation (a fired employee, a compromised account), you need a block list consulted on every request. And then you have server state again, which is exactly what the JWT promised to avoid.
The conclusion that follows: in many systems, a traditional session in Redis is simpler, safer and just as fast. JWT solves one specific problem: validating without consulting the issuer, in a distributed architecture or across organisations. If you do not have that problem, you may not need it.
Where to store the token
Two options, with real trade-offs:
HttpOnly + Secure + SameSite cookie: JavaScript cannot read it, so an XSS does not steal the token. Vulnerable to CSRF, mitigated by SameSite. It is the standard recommendation for web applications.
localStorage: any XSS reads it. No CSRF exposure. It is common in SPAs and is a worse choice than most people assume.
If you store tokens in localStorage, your low severity XSS just became account compromise.
Authorisation models
Finishing with the "what you are allowed to do".
RBAC, role based. Admin, editor, reader. Simple, and it turns into a mess once the special cases arrive: "editor, but only for department X", "reader, except financial data". The answer is usually to create more roles, and in two years you have two hundred.
ABAC, attribute based. The rule evaluates attributes: department, transaction amount, time of day, location. More expressive, harder to audit: answering "who can see this?" requires evaluating rules.
ReBAC, relationship based. "You can edit this document because you own the folder that contains it." It is Google's Zanzibar model, and it is what solves hierarchical sharing without exploding into roles. Open implementations exist (SpiceDB, OpenFGA, Ory Keto).
For most systems, RBAC with a few attributes is enough. ReBAC pays off when sharing and hierarchy are a core feature of your product.
The five minute checklist
In your code, today:
→ Is the JWT algorithm pinned on the server?
→ Are aud and iss validated?
→ What is the access token TTL?
→ Is there a revocation path, and has it been tested?
→ Is the token in an HttpOnly cookie or in localStorage?
→ Is the OAuth flow Authorization Code with PKCE?
→ Is authorisation checked on the server for every endpoint, or only the ones that go through the UI?
Read this next
- EngineeringStep 20Legacy: characterise, seam and strangleThree techniques that let you safely change a system you did not write, do not understand, and cannot stop.Read article
- EngineeringStep 19Tests worth what they costTests do not exist to prove the code is right. They exist so you can change it tomorrow without fear. That change of goal reorganises everything.Read article
- EngineeringStep 17The vulnerabilities that actually show upMost breaches that make the news do not use sophisticated technique. They use one of these six flaws, and all six have a known, cheap fix.Read article