---
title: "OAuth vs JWT"
slug: "oauth-vs-jwt"
category: "comparisons"
tags: ["comparisons", "oauth", "jwt", "authentication", "authorization", "security"]
status: "stable"
last_updated: 2026-05-14
summary: "OAuth is a delegation protocol; JWT is a token format. Use OAuth for third-party authorization; use JWT for signed stateless tokens, never as a session store."
related: ["[[backend/fastapi]]", "[[backend/postgres]]", "[[comparisons/rest-vs-graphql]]", "[[comparisons/redis-vs-memcached]]", "[[glossary/idempotent]]", "[[frontend/nextjs]]", "[[comparisons/dynamodb-vs-postgres]]"]
---

## 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 [[comparisons/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.

- `localStorage` is 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 `Secure` cookie, 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.

## Related

- [[backend/fastapi]]
- [[backend/postgres]]
- [[comparisons/rest-vs-graphql]]
- [[comparisons/redis-vs-memcached]]
- [[glossary/idempotent]]
- [[frontend/nextjs]]
- [[comparisons/dynamodb-vs-postgres]]
