Overview
OAuth and JWT are not alternatives; they are different concepts that are often confused because JWT is a common token format used inside OAuth flows. OAuth 2.0 is a delegation protocol that lets a user grant a third-party application limited access to a resource without sharing credentials. JWT (JSON Web Token) is a compact, signed token format for encoding claims. The rule is: use OAuth when a user must delegate access to a third-party app. Use JWT for short-lived, stateless claims transport between services. Never use a JWT as a session token stored in localStorage and refreshed indefinitely. That pattern recreates the security problems of sessions without the revocation mechanisms sessions provide. Use opaque session tokens plus a session store (Redis) for user sessions. See redis-vs-memcached for the session store choice.
When OAuth wins
OAuth is the right protocol for delegation flows involving a resource owner, a client, and an authorization server.
- “Sign in with Google/GitHub/Apple”: the user authorizes your app to read their profile. Your app receives an access token scoped to that permission. This is OAuth (specifically the Authorization Code flow with PKCE).
- API access on behalf of a user: a CI tool needs read access to a user’s repos. The user grants it via OAuth; the tool stores the token and uses it to call the API. Scope limits what the token can do.
- Machine-to-machine service authorization without user context: Client Credentials flow issues a token to a service identity, not a user. Scopes restrict what the service can call.
- Refresh token rotation: OAuth defines a refresh flow where a short-lived access token can be refreshed without re-prompting the user. The refresh token is revocable at the authorization server.
- Revocation: OAuth tokens are tracked by the authorization server; you can revoke them. JWTs, once issued, are valid until expiry unless you maintain a blocklist.
When JWT wins
JWT is the right format for short-lived, stateless claims that do not require server-side state.
- Service-to-service authentication inside a trust boundary: Service A issues a signed JWT asserting “caller: service-a, scope: read-orders”; Service B validates the signature without a database lookup.
- Short-lived access tokens inside an OAuth flow: the access token issued by an OAuth authorization server is often a JWT. The resource server validates it by checking the signature and expiry.
- Claims propagation in a microservices chain: the gateway verifies the user’s session and issues a JWT with user ID, role, and tenant ID. Downstream services read claims without a session lookup.
- Stateless API access with short TTL (15 minutes): issue a JWT, let it expire, require re-authentication. No revocation needed because the window is narrow.
The JWT-as-session antipattern
Storing a long-lived JWT in localStorage and refreshing it on the client is a common antipattern. It trades the security of HTTP-only cookies and server-side revocation for a stateless token that cannot be invalidated if compromised.
localStorageis accessible to JavaScript; an XSS vulnerability exfiltrates the token.- A 30-day JWT cannot be revoked without a blocklist, which negates the statelessness benefit.
- The correct pattern for user sessions: issue an opaque session token, store it in an HTTP-only
Securecookie, keep session state server-side in Redis with a short TTL and sliding expiry. - JWTs are for service-level claims transport, not user session persistence.
Trade-offs at a glance
| Dimension | OAuth 2.0 | JWT (standalone) |
|---|---|---|
| Purpose | Delegation protocol | Token format for claims |
| Statefulness | Authorization server tracks tokens | Stateless; no server lookup needed |
| Revocation | First-class; revoke at auth server | Requires blocklist; complex |
| User consent flows | Built-in (Authorization Code, Device flow) | Not a concept |
| Refresh token | Standard; rotation supported | Implement yourself |
| Scope/permission model | Built-in (space-separated scope strings) | Custom claims |
| Signature | HMAC or RSA on the access token (often JWT) | HMAC-SHA256 or RS256 |
| Expiry enforcement | Authorization server checks | Validator checks exp claim |
| Best use case | Third-party delegation, user consent | Service-to-service, short-lived claims |
| Worst use case | Internal service auth without a user | Long-lived user sessions |
Migration cost
Replacing JWTs-as-sessions with proper session management is the most common migration.
- JWT sessions to opaque sessions: introduce a session store (Redis), generate opaque tokens, set HTTP-only cookies, add a session middleware. Client-side changes are minimal if the token was in a cookie; significant if stored in
localStorage. Plan one engineer-week. - OAuth-to-JWT service auth: replace OAuth token endpoint calls with JWT issuance at the calling service. Eliminate the authorization server for internal traffic. Lower operational complexity; trade revocation for simplicity. Plan two to three days per service.
- Adding OAuth to an existing password-auth app: add an OAuth provider (Auth0, Clerk, Supabase Auth) rather than building the authorization server. Plan one engineer-week for integration.
Recommendation
- “Sign in with X” or third-party API delegation: OAuth 2.0 Authorization Code with PKCE. Do not roll your own.
- User sessions in a web app: opaque session token in an HTTP-only cookie; server-side store in Redis. Do not use JWT.
- Service-to-service auth inside a trusted network: short-lived JWT (15-minute TTL) signed with a shared secret or rotating key pair.
- Public API issuing access tokens to developers: OAuth Client Credentials; tokens scoped per client.
- Multi-tenant SaaS with per-tenant scopes: JWT with tenant and role claims in the payload; validate at each service.