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.

  • 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

DimensionOAuth 2.0JWT (standalone)
PurposeDelegation protocolToken format for claims
StatefulnessAuthorization server tracks tokensStateless; no server lookup needed
RevocationFirst-class; revoke at auth serverRequires blocklist; complex
User consent flowsBuilt-in (Authorization Code, Device flow)Not a concept
Refresh tokenStandard; rotation supportedImplement yourself
Scope/permission modelBuilt-in (space-separated scope strings)Custom claims
SignatureHMAC or RSA on the access token (often JWT)HMAC-SHA256 or RS256
Expiry enforcementAuthorization server checksValidator checks exp claim
Best use caseThird-party delegation, user consentService-to-service, short-lived claims
Worst use caseInternal service auth without a userLong-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.