By AndyPublished
JWT vs OAuth: Token Format vs Authorisation Framework
So the practical questions are different ones: should your OAuth access tokens be JWTs or opaque, and do you need OAuth at all, or just signed tokens issued by your own login endpoint? This guide answers both, and places SAML and API keys on the same map.
JWT vs OAuth 2.0: Different Layers of the Stack
The easiest way to see the difference is to ask what each specification would say if you removed the other. Remove JWT from OAuth and OAuth still works: the authorisation server issues a random string, stores what it means, and the API asks the server about it. Remove OAuth from JWT and JWT still works: your own login route signs {"sub":"42","exp":...} with a secret and your API verifies it. Neither depends on the other.
| Aspect | JWT | OAuth 2.0 |
|---|---|---|
| What it is | A token format: signed (or encrypted) JSON claims | An authorisation framework: roles, grant types, endpoints |
| Specification | RFC 7519, built on JWS (RFC 7515) and JWE (RFC 7516) | RFC 6749, with Bearer usage in RFC 6750; OAuth 2.1 is in draft |
| Answers the question | How is this set of claims packaged and protected? | How does a client get permission to call an API on a user’s behalf? |
| Moving parts | Header, payload, signature | Resource owner, client, authorisation server, resource server |
| Can exist without the other | Yes: session tokens, email links, service-to-service calls | Yes: many providers issue opaque access tokens |
| Typical output | eyJhbGciOi... (three dot-separated segments) | An access token, optional refresh token, expires_in, scope |
OAuth is about delegation: a user lets a third-party client act on their behalf with a limited scope, without handing over a password. JWT is about integrity: whoever receives the token can check that the claims were issued by a known key and have not been changed. The structure of a JWT is covered in detail in what is a JWT.
Is JWT OAuth?
No. A JWT is not OAuth, and using JWTs does not mean you are “doing OAuth”. The confusion comes from the fact that the two are so often seen together:
- ·Access tokens: Auth0, Okta, Microsoft Entra ID, AWS Cognito and Keycloak issue JWT access tokens (in some cases only when you request a specific API audience or resource). RFC 9068 standardises a JWT profile for them.
- ·ID tokens: OpenID Connect, the identity layer on top of OAuth 2.0, requires the ID token to be a JWT. This is the one place in the OAuth family where JWT is mandatory.
- ·Client authentication: RFC 7523 lets a client prove its identity to the token endpoint with a signed JWT instead of a client secret (
private_key_jwtin OIDC terms). - ·Proof of possession: DPoP (RFC 9449) uses a small signed JWT on each request to bind an access token to a key.
JWT is the preferred serialisation across the OAuth ecosystem, but the framework itself is format-agnostic. For how each of these tokens is used in practice, see JWTs in OAuth 2.0 and OpenID Connect.
How JWT Fits Into an OAuth Flow
Take the authorisation code flow with PKCE, which is the recommended flow for web, mobile and single-page apps. The JWTs appear at specific points:
1. Client → browser redirect to /authorize?response_type=code&client_id=...
&scope=openid profile orders:read&code_challenge=...&code_challenge_method=S256
2. User logs in and consents at the authorisation server
3. Server → redirect back to client with ?code=...
4. Client → POST /token grant_type=authorization_code&code=...&code_verifier=...
5. Server → {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIs...", ← JWT (or opaque)
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ij...", ← always a JWT (OIDC)
"refresh_token":"8xLOxBtZp8", ← usually opaque
"token_type": "Bearer",
"expires_in": 900
}
6. Client → GET /orders Authorization: Bearer <access_token>
7. API validates the access token and applies scope and audience checksStep 7 is where the format matters. If the access token is a JWT, the API verifies it itself. If it is opaque, the API has to ask the authorisation server. The client should treat the access token as opaque either way: it is meant for the API, and its format can change without notice. The client reads the ID token, not the access token.
aud is the client ID rather than the API identifier, and it carries no scopes. APIs should accept access tokens only. The wrong audience error is the usual symptom.JWT vs Opaque Token for OAuth2 Access Tokens
This is the real decision hiding behind most “JWT vs OAuth access token” searches. An opaque token is a high-entropy random reference, such as tGzv3JOkF0XG5Qx2TlKWIA, whose meaning lives only in the authorisation server’s database. A JWT access token carries its meaning inside itself.
| Aspect | JWT access token | Opaque access token |
|---|---|---|
| Validation | Locally: signature against the issuer’s JWKS, then iss, aud, exp | Remotely: call the introspection endpoint (RFC 7662), or look it up in a shared store |
| Latency per request | No network call once keys are cached | One round trip per request unless you cache the result |
| Revocation | Not visible until exp unless you add a deny-list or introspect | Immediate: the server simply stops reporting it as active |
| Who can read the claims | Anyone holding the token (payload is Base64url, not encrypted) | Nobody except the authorisation server |
| Size | Hundreds of bytes to several KB | Usually 20–80 characters |
| Best fit | Many APIs, microservices, high request volume | Public clients, sensitive claims, instant revocation |
JWT access tokens: RFC 9068
RFC 9068 defines a common profile so resource servers can validate JWT access tokens from any compliant issuer. The header uses typ: at+jwt so an access token cannot be confused with an ID token, and the payload must include iss, exp, aud, sub, client_id, iat and jti, with scope when scopes were granted. Validation with the jose library looks like this:
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));
export async function validateAccessToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.example.com/',
audience: 'https://api.example.com',
typ: 'at+jwt', // rejects ID tokens (typ "JWT")
algorithms: ['RS256', 'ES256'],
});
const scopes = String(payload.scope ?? '').split(' ');
return { sub: payload.sub, clientId: payload.client_id, scopes };
}Not every provider sets typ: at+jwt yet; check a real token before enforcing it. The full check order is in the JWT verification guide.
Opaque tokens: introspection (RFC 7662)
With an opaque token, the API posts it to the authorisation server’s introspection endpoint and receives the metadata back. The API authenticates itself on that call, typically with its own client credentials:
curl -s https://auth.example.com/oauth2/introspect \
-u "orders-api:API_CLIENT_SECRET" \
-d "token=tGzv3JOkF0XG5Qx2TlKWIA" \
-d "token_type_hint=access_token"
{
"active": true,
"scope": "orders:read",
"client_id": "web-app",
"sub": "user_123",
"aud": "https://api.example.com",
"exp": 1790000900
}active is the only required field in the response. A revoked, expired or unknown token simply returns {"active": false}, which is why opaque tokens give instant revocation. Revoking a token at the server is standardised separately in RFC 7009.
Choosing between them
- ·Choose JWT when many services validate tokens, request volume is high, and short lifetimes (5 to 15 minutes) are acceptable in place of instant revocation.
- ·Choose opaque when tokens go to third-party or public clients who should not read the claims, when claims are sensitive, or when revocation must take effect immediately.
- ·A common hybrid is the “phantom token” pattern: the outside world gets an opaque token, and an API gateway introspects it once and forwards a JWT to internal services.
The revocation trade-off is the same one covered in JWT vs session tokens: self-contained tokens trade freshness for scalability.
JWT vs OAuth: Which Is Better?
Because they solve different problems, the useful question is whether you need OAuth’s delegation machinery.
- ·You need OAuth (usually with OIDC) when third-party apps call your API on behalf of your users, when you want single sign-on across several applications, when you use an external identity provider, or when you need standard flows for mobile, SPA and machine clients.
- ·You probably do not need OAuth when one first-party web app talks to its own backend. A server-side session cookie, or a short-lived JWT issued by your own login endpoint, is simpler and has fewer moving parts.
- ·Do not build your own OAuth server for the sake of it. If you need OAuth, use a maintained authorisation server (a hosted provider, Keycloak, or a certified library).
Whichever you choose, the token rules are the same: pin the algorithm, check iss, aud and exp, keep lifetimes short. See JWT security best practices.
JWT vs OAuth vs SAML
SAML 2.0 is the third name in this comparison, and it overlaps with both. SAML is a complete federation protocol whose tokens (assertions) are XML documents signed with XML Signature. It is built for browser single sign-on into enterprise applications, and it is still widely used for that. OAuth handles API authorisation; OpenID Connect handles modern sign-on and uses JWTs as its token format. Put simply, SAML competes with OpenID Connect, and SAML assertions compete with JWT ID tokens. The full comparison is in JWT vs SAML.
JWT vs OAuth2 vs API Key
An API key is a long-lived static secret that identifies a calling application. It has no expiry, no user context and no standard format. The three fit together like this:
- ·API key: simplest; good for server-to-server calls by a known customer, usage metering and rate limiting. Revoke by deleting the key.
- ·JWT: short-lived, signed claims about a user or service; validated without a database lookup.
- ·OAuth 2.0: the process for issuing tokens (often JWTs) with scopes and user consent. The client credentials grant is the OAuth replacement for a static API key in machine-to-machine traffic.
More detail on where static keys still win is in JWT vs API key.
How to Tell What Kind of Token You Have
If you are handed an access token and are not sure whether it is a JWT, check its shape. Three Base64url segments starting with eyJ is a signed JWT; five segments is an encrypted JWE; no dots at all means an opaque token that only the issuer can interpret. Paste a JWT into the jwtdecode.app decoder, which runs in your browser, to read typ, aud, scope and client_id and see whether it is an access token or an ID token.
Common Misconceptions
- ·“OAuth is for authentication.” Plain OAuth 2.0 tells an API what a client may do, not who the user is. Logging users in is what OpenID Connect adds, through the ID token and the
/userinfoendpoint. - ·“A JWT access token is encrypted.” A signed JWT is only encoded; anyone holding it can read the payload. Keep personal data out of it, or use an opaque token. See is a JWT encrypted?
- ·“Clients should parse the access token.” The access token belongs to the API. A client that reads claims from it breaks when the provider switches formats; clients should read the ID token or call
/userinfo. - ·“JWTs replace refresh tokens.” Short-lived JWT access tokens usually depend on a refresh token to stay usable, and the refresh token is often opaque and stored server-side. See the refresh token pattern.
Summary
- ·OAuth 2.0 is an authorisation framework; JWT is a token format. They are complementary, not alternatives.
- ·OAuth access tokens can be JWTs (profiled by RFC 9068) or opaque strings validated through introspection (RFC 7662).
- ·OpenID Connect ID tokens are always JWTs, but they are for the client, not the API.
- ·JWT access tokens scale well and validate locally; opaque tokens revoke instantly and hide their claims.
- ·Use OAuth when you need delegation, SSO or third-party access; otherwise a session or a simple signed token is often enough.