The short answer
OAuth 2.0 (RFC 6749) is an authorisation framework. It defines a set of flows by which a client application obtains a token that lets it access a resource owner's data — crucially, without the client ever handling that person's password. It describes endpoints, redirects, parameters and grant types. It deliberately says nothing about what the token contains.
JWT (RFC 7519) is a token format. It defines a compact, URL-safe way to represent a set of claims as a signed, base64url-encoded JSON structure. It describes bytes. It says nothing about how a client got hold of one.
So the two combine rather than compete. An OAuth authorisation server may issue JWTs as access tokens, or it may issue opaque random strings — both are conformant. Equally, a system with a single monolithic login and no authorisation server anywhere can happily issue JWTs to its own users. Asking “OAuth or JWT?” is like asking “courier service or envelope?”.
| OAuth 2.0 | JWT | |
|---|---|---|
| Category | Authorisation framework — a protocol | Token format — a data structure |
| Specification | RFC 6749, plus RFC 6750 for bearer usage | RFC 7519, on top of JWS (RFC 7515) |
| What it specifies | Roles, endpoints, redirects, grant types, parameters, error responses | Header, payload claims, signing, base64url encoding |
| What it does not specify | The token's format or contents | How the token is obtained, stored or transmitted |
| Answers the question | “How does the client get a token?” | “What does the token look like?” |
| Typical use | Delegated access, third-party integrations, SSO | Access tokens, ID tokens, stateless sessions, service-to-service claims |
| Can exist without the other | Yes — OAuth with opaque tokens | Yes — JWTs in a system with no OAuth |
A third piece completes the picture: whatever token you end up with usually travels as
Authorization: Bearer <token>. That transport layer is covered in the
bearer token guide, and the broader landscape of schemes in the
API authentication overview.
OAuth 2.0 properly
OAuth exists to solve one problem: letting an application act on your behalf against a service, with limited permissions, without you giving that application your password. Before it, the “solution” was to hand a third-party site your bank or email credentials and hope. OAuth replaces that with a delegated grant.
The four roles
| Role | Who it is |
|---|---|
| Resource owner | The user who owns the data and can grant access to it |
| Client | The application requesting access — a web app, SPA, mobile app or service |
| Authorisation server | Authenticates the resource owner, obtains consent, issues tokens |
| Resource server | The API holding the data; validates tokens and serves requests |
The authorisation server and resource server are often operated by the same company but are logically separate, and separating them is what makes tokens portable. Clients are further split into confidential clients, which can keep a secret (a backend service), and public clients, which cannot (SPAs and mobile apps, whose code the user can read).
The grant types that matter today
| Grant | Use it for | Status |
|---|---|---|
| Authorization Code + PKCE (RFC 7636) | Any flow involving a user: server-rendered apps, SPAs, mobile | The default; PKCE recommended for all client types |
| Client Credentials | Machine-to-machine, no user present | Current |
| Refresh Token | Obtaining a fresh access token without re-prompting | Current; rotate for public clients |
| Device Authorization Grant (RFC 8628) | TVs, consoles, CLIs — devices with no browser or keyboard | Current |
| Implicit | Formerly SPAs | Deprecated by the OAuth 2.0 Security BCP |
| Resource Owner Password Credentials | Formerly first-party apps | Deprecated by the OAuth 2.0 Security BCP |
Take those last two seriously. Implicit returned the access token directly in the redirect
URL fragment, exposing it to browser history, the Referer header and any script on the page;
Authorization Code with PKCE now covers that case properly. Password grant requires the client
to collect the user's actual username and password, which destroys the entire premise of OAuth and is
incompatible with multi-factor authentication and federated login. Both still appear in older tutorials. Do
not treat them as live options.
The authorization code flow, step by step
This is the flow worth knowing in detail, because everything else is a variation on it.
- The client generates a random
code_verifierand derivescode_challenge = BASE64URL(SHA256(code_verifier)), and separately generates a randomstatevalue it stores in the user's session. - It redirects the browser to the authorisation endpoint with
response_type=code,client_id,redirect_uri,scope,state,code_challengeandcode_challenge_method=S256. - The authorisation server authenticates the user — password, passkey, MFA, whatever it uses — and shows a consent screen listing the requested scopes.
- On approval it redirects back to
redirect_uriwithcodeand the samestate. The code is short-lived and single-use. - The client checks that
statematches what it stored, thenPOSTs to the token endpoint withgrant_type=authorization_code,code,redirect_uri,client_idand the originalcode_verifier— asapplication/x-www-form-urlencoded, not JSON, which is a classic first-attempt failure. - The authorisation server hashes the
code_verifier, compares it with the storedcode_challenge, and on success returnsaccess_token,token_type(Bearer),expires_in, usuallyrefresh_tokenand, ifopenidwas requested,id_token.
The two security parameters defend against different attacks and neither substitutes for the other.
state defends the redirect against cross-site request forgery: without it an
attacker can feed a victim's browser an authorisation response for the attacker's own account, silently
linking the victim's session to attacker-controlled data. PKCE defends against
authorization code interception: on a mobile device a malicious app can register the same
custom URL scheme and capture the redirect, and on any platform the code may leak through logs or the
Referer. A stolen code is useless without the code_verifier, which never leaves the
client. PKCE was originally designed for mobile; it is now recommended for confidential clients too.
OAuth 2.1 is a draft that consolidates RFC 6749, the security BCP and several extensions into one document — PKCE required everywhere, implicit and password grants removed, exact redirect-URI matching, bearer tokens out of query strings. It is work in progress at the IETF and not a published RFC, so cite it as direction of travel rather than as a standard to conform to. Following it today is nonetheless good advice, because it mostly codifies what the BCP already recommends.
OpenID Connect: the missing identity layer
Here is the gap that causes more real bugs than the OAuth-vs-JWT confusion itself. An OAuth access token tells you nothing about who the user is. It is a capability — “the holder may read this mailbox” — not an assertion of identity. It carries no guarantee about when, how, or even whether a human authenticated.
OpenID Connect (OIDC) is the identity layer built on top of OAuth 2.0 that fills the gap. It
adds four things: the openid scope that requests identity; the id_token,
which is always a JWT and which asserts that a specific user authenticated at a specific time at a
specific issuer; a UserInfo endpoint returning profile claims for a given access token; and
discovery, a well-known JSON document advertising endpoints, supported scopes and the JWKS
URL for signing keys. The classic one-liner is accurate:
OAuth is authorisation, OIDC is authentication.
This is also where OAuth and JWT stop being independent: an id_token is required to be a JWT.
Everywhere else the format is a free choice; here it is not.
Why “log in with an access token” is an anti-pattern
The broken pattern goes: the client obtains an access token from a provider, calls the provider's profile API, gets back an email address, and logs the user in as whoever owns that address. The flaw is that the client never verified who the token was issued to. An access token is a bearer credential — see the bearer token guide — so any application the same user authorised can take the token it legitimately received and present it to your API to impersonate that user. This is the confused-deputy problem that gave rise to real-world “log in as anyone” vulnerabilities in early social login integrations.
An id_token closes it because it is addressed. Its aud claim names your
client_id, its iss names the issuer, its nonce ties it to your specific
authentication request, and its signature proves the issuer produced it. Validate all of those and a token
minted for another application simply fails the check. If you need identity, use OIDC and validate the
id_token — do not infer identity from an access token.
JWT, briefly
A JWT is three base64url segments separated by dots:
header.payload.signature. The header names the algorithm and usually a key id; the payload holds
the claims; the signature covers the first two segments joined by a dot. Registered claims from RFC 7519
include iss (issuer), sub (subject), aud (audience), exp
(expiry), nbf (not before), iat (issued at) and jti (a unique id, which
is what a denylist keys on).
Two facts do most of the work in practice. First, the payload is encoded, not encrypted — base64url is a reversible transformation with no key, so anyone who holds the token can read every claim in it. Never put a secret, a password, an internal identifier you would not publish, or personal data you would not email in clear text into a JWT payload. Second, signing choice matters: HS256 uses a shared secret, so every party that can verify can also forge; RS256 and ES256 are asymmetric, so verifiers hold only a public key. The moment a separate party verifies your tokens, asymmetric is the right answer.
JWT is one member of the wider JOSE family. JWS (RFC 7515) is the signed structure that ordinary JWTs use; JWE (RFC 7516) is the encrypted one, and it genuinely hides the payload at the cost of complexity and of the resource server needing a decryption key. Alongside them sit JWK for key representation and JWA for algorithm identifiers.
This page deliberately stops there. The JWT decoder covers claim-by-claim
detail, the alg confusion and none-algorithm attacks, HS256 versus RS256 versus
ES256, storage, and how to read a token you have been handed — and it will decode one for you while you read.
The real trade-off: self-contained vs reference tokens
Strip away the terminology and the genuine architectural decision is this: should a resource server be able to validate a token on its own, or should it ask the issuer?
| Dimension | Self-contained (JWT) | Reference (opaque) |
|---|---|---|
| Validation | Local signature check, microseconds, no network | Lookup or introspection call per request, or a cache with its own staleness |
| Scaling | Excellent — no shared session store on the hot path | Bounded by the store; needs caching and capacity planning |
| Revocation | Effective only at exp, unless you add a denylist | Immediate — delete the record |
| Claim freshness | Stale from the instant of issue; a role removed mid-token is still in the token | Always current |
| Size | Grows with claims; can bloat headers past proxy limits | Small and constant |
| Confidentiality | Claims readable by anyone holding the token | Nothing to read |
| Failure mode | Issuer outage does not stop validation | Store outage stops everything |
JWT revocation is the hard problem, and it should not be glossed over. The property that
makes a JWT fast — offline validation — is exactly the property that makes revocation awkward. If a resource
server validates locally, then a token with a valid signature and an exp in the future
will be accepted, whatever has happened at the issuer since. The user logged out; the account was
suspended; an admin removed a role. None of that reaches the resource server. Three mitigations exist, and
each costs something:
- A denylist keyed by
jti. Works, and reintroduces the shared state you chose JWTs to avoid — though only for revoked tokens until they expire, which is a much smaller store than a session table. - Very short lifetimes plus refresh. A five-minute access token bounds the exposure window, and revocation is enforced at the refresh, where you already have state. This is the mainstream answer, and it is a bound rather than a fix.
- Token introspection (RFC 7662). Ask the authorisation server whether the token is still
active— at which point you have the per-request lookup you were avoiding, and the token being a JWT has stopped buying you much.
Notice that none of these is a clean win, which is the honest conclusion: pick based on whether immediate revocation or independent validation matters more for the thing you are protecting. Many mature systems do both — JWTs for high-volume read APIs, an introspected or opaque token for anything that moves money.
Decision guide by scenario
| What you are building | What to reach for |
|---|---|
| “Let users sign in with Google / Microsoft / Apple” | OIDC on top of OAuth 2.0. Authorization Code + PKCE, openid scope, validate the id_token's iss, aud, exp and nonce. Do not roll this yourself. |
| Your own SPA talking to your own API | Authorization Code + PKCE against your own authorisation server. Token format is genuinely your choice — JWT if you have several services validating, opaque if one gateway does it. |
| Service-to-service inside one cluster | Client Credentials, or mTLS if the mesh already provides it. Either token format works; a short-lived JWT with a narrow aud avoids a shared lookup on every hop. |
| Third-party developers accessing your users' data | OAuth 2.0, unambiguously. This is the exact problem it was invented for: scoped, consented, revocable delegated access without your users handing over passwords. |
| A single monolith with its own login | OAuth is probably ceremony you do not need. A server-side session with an httpOnly, Secure, SameSite cookie is simpler, revocable by default and hard to get wrong. A signed token is fine too. |
| A CLI or a smart TV app | Device Authorization Grant (RFC 8628) — the device shows a code, the user approves it on a phone, the device polls the token endpoint. |
| A cron job hitting your own API | Client Credentials, or a scoped API key in a secrets manager. No user, so no user-facing flow is needed. |
Two pieces of pragmatism worth stating plainly. First, OAuth is frequently overkill. If there
is no third party, no delegated access and no cross-domain identity, adding an authorisation server buys you
complexity, redirect debugging and a new outage surface in exchange for benefits you are not using. Second,
when OAuth is right, use a maintained library or a hosted provider. The flows are simple to describe
and easy to implement subtly wrong — redirect-URI matching, state handling, token storage — and
each mistake is a full account takeover. Whichever route you take, the end state is the same: a token you can
paste into the API tester to confirm the resource server accepts it before you wire it into
an application.
Common misconceptions
“OAuth is authentication”
It is not. OAuth answers “may this client do this thing?”, not “who is this person?”. The identity layer is
OpenID Connect, and the artefact that carries identity is the id_token. Treating an access token
as proof of login is the confused-deputy anti-pattern described above.
“JWTs are secure by default”
A JWT is secure only if you validate it properly: check that alg matches what you expect rather
than trusting the header, that iss is your issuer, that aud names your service, and
that exp and nbf are satisfied with a small clock leeway. Skip the aud
check and any token from the same issuer works against your API. Skip the alg check and you are
exposed to algorithm confusion — the JWT decoder walks through exactly how
those attacks work.
“JWTs are encrypted”
No. A signed JWT is a JWS; the payload is base64url-encoded and readable by anyone who has the token. Signing proves integrity and origin, not confidentiality. JWE (RFC 7516) is the encrypted variant, and it is a different structure with a different shape.
“OAuth means JWT”
RFC 6749 never mentions a token format. GitHub, among many others, issues opaque access tokens, and RFC 7662 introspection exists precisely because opaque OAuth tokens are common. If you assume every OAuth token can be decoded, your integration will break the first time you meet one that cannot.
“Stateless means no database”
Stateless means the token carries its own claims so validation needs no lookup. You still have a database — for users, permissions, refresh tokens, revocations and audit. What JWTs remove is one lookup on the request hot path, which is a real performance win and not an architectural exemption.
“Longer expiry is more convenient”
It trades a user-experience problem you can solve for a security problem you cannot. Silent refresh solves the convenience; nothing solves a long-lived stolen bearer token except waiting for it to expire.
Seeing it work
Both halves are easier to believe once you have watched them. For the JWT half, take any token you have and
drop it into the JWT decoder — you will see the header, every claim and the
expiry, without a key, which makes the “encoded, not encrypted” point better than any paragraph can. For the
OAuth half, take an access token you already hold and call a protected endpoint from the
API tester with the Auth tab set to Bearer; then compare what happens with no token
(401, with a WWW-Authenticate challenge) and with a valid
token missing a scope (403). Token endpoints themselves are worth
exercising in the REST API tester: they take
application/x-www-form-urlencoded bodies, and seeing the raw response teaches more than reading
about expires_in.
From here: the bearer token guide covers how the token actually travels and
how to stop leaking it, the API authentication guide surveys the
other schemes, and the HTTP headers reference explains the
Authorization and WWW-Authenticate fields in their wider context.
Frequently asked questions
Is OAuth better than JWT?
Neither, because they are not alternatives. OAuth 2.0 is a framework for how a client obtains a token; JWT is a format for what a token looks like. An OAuth server can issue JWTs or opaque tokens, and JWTs appear in plenty of systems with no OAuth at all.
Can you use OAuth without JWT?
Yes. RFC 6749 never specifies a token format, so an authorisation server may issue opaque strings that resource servers validate via the RFC 7662 introspection endpoint. GitHub is a well-known example.
Is OAuth 2.0 authentication or authorisation?
Authorisation. An access token grants delegated access; it asserts nothing reliable about identity.
OpenID Connect is the identity layer on top, adding the id_token, the openid scope
and a UserInfo endpoint.
Are JWTs encrypted?
A signed JWT is not. The payload is base64url-encoded and readable by anyone holding the token. Encryption requires JWE (RFC 7516), a separate structure. Never put secrets in a signed JWT payload.
How do you revoke a JWT?
There is no clean way. Offline validation accepts any correctly signed, unexpired token, so you need a
denylist keyed by jti, very short lifetimes with refresh, or introspection on every request — and
the last two reintroduce the state you were avoiding.
Which OAuth grant type should I use?
Authorization Code with PKCE whenever a user is involved, Client Credentials for machine-to-machine, and the Device Authorization Grant for TVs and CLIs. The Implicit and Password grants are deprecated by the OAuth 2.0 Security Best Current Practice.