JWT vs OAuth: Understanding Authentication for Modern Applications
JWT and OAuth are often compared as alternatives. They are not — one is a token format, the other is an authorisation framework. Here is how they fit together.
Starter draft. The explanations are general; add examples from your own integrations before publishing.
"Should we use JWT or OAuth?" is a question I have been asked in several code reviews, and it contains a category error. They are not alternatives.
- JWT is a token format — a way to encode claims so they can be verified.
- OAuth 2.0 is an authorisation framework — a set of flows for one application to obtain limited access to a resource on a user's behalf.
- OpenID Connect (OIDC) is an authentication layer on top of OAuth 2.0, which is what most people actually mean when they say "sign in with OAuth".
A typical enterprise setup uses all three: OIDC to sign the user in, OAuth 2.0 to issue an access token, and JWT as the format of that token.
What a JWT actually is#
Three Base64url-encoded parts separated by dots: header, payload, signature.
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTEyMyIsImV4cCI6MTc2NzIyNTYwMH0.<signature>
The payload decodes to ordinary JSON:
{ "sub": "user-123", "iss": "https://login.example.com", "aud": "api://orders", "exp": 1767225600, "roles": ["orders.read", "orders.write"] }
The part people most often get wrong: a JWT payload is encoded, not encrypted. Anyone holding the token can read it. Never put anything in a JWT you would not be willing to show the person carrying it.
What makes it useful is the signature: the API can verify that the token was issued by the expected issuer and has not been altered, without calling back to the identity provider on every request.
Verifying a JWT properly#
Decoding is not verification. A complete check covers:
- Signature, against the issuer's public key (fetched from its JWKS endpoint and cached).
- Algorithm, against an allow-list. Never trust the
algheader to tell you what to do — that is howalg: noneattacks work. issmatches the expected issuer.audmatches this API. A token minted for another service is not valid here.exp/nbf, with a small clock-skew allowance.
Frameworks do this for you when configured correctly, which is the argument for using the platform's own middleware instead of hand-rolling verification.
What OAuth 2.0 adds#
OAuth exists to answer a different question: how does an application get a token at all, without handling the user's password?
The flows worth knowing:
| Flow | Use it for |
|---|---|
| Authorization Code + PKCE | Web apps, SPAs and mobile apps signing a user in |
| Client Credentials | Service-to-service, no user involved |
| Device Code | Devices without a usable browser or keyboard |
Two flows you should not reach for: the implicit flow (superseded by Authorization Code with PKCE) and the resource owner password credentials flow (the application handles the user's password, which defeats most of the point).
Where sessions still make sense#
Stateless tokens are not automatically the right choice.
Server sessions are simpler for a traditional server-rendered app: revocation is immediate, nothing sensitive lives in the browser, and there is no refresh dance. Their cost is shared session state.
JWTs suit APIs with multiple clients and no shared session store, and they are what identity providers issue. Their cost is real: a signed token stays valid until it expires. You cannot un-issue it. Mitigations — short lifetimes with refresh tokens, a revocation list for high-value cases, a jti claim you can check — all reintroduce some state.
Choose on that trade-off, not on which is more modern.
Practical rules#
- Do not implement OAuth yourself. Use the identity provider's libraries — MSAL for Microsoft Entra ID, or the framework's middleware.
- Keep access tokens short-lived. Minutes, not days.
- Validate
audandisson every API. This is the check most often forgotten. - Never put secrets, personal data or anything sensitive in a JWT payload.
- Authorise on claims, server-side. The UI reading a role claim is convenience; the API checking it is the control.
- Use HTTPS everywhere. A bearer token is a bearer token — whoever holds it, uses it.
A decoded token proves nothing#
If you take one thing from this: being able to decode a token and read a roles claim does not mean the token is genuine. Only signature verification does. Anyone can craft a JWT with any payload they like — the signature is the entire security boundary.
You can inspect a token's claims with the JWT decoder on this site. Like every tool here, it runs in your browser — and, like every decoder, it does not verify anything.