Skip to content

Sessions, JWTs, and OAuth/OIDC: Three Different Jobs

Core Concept LearningJuly 4, 20265 min readUpdated July 21, 2026

You log out and the admin panel still accepts the old token. Or you build "Sign in with Google" and accidentally treat an access token as proof of identity. Auth failures feel like product bugs until you realize you mixed three different models.

This article is the credential-model comparison: server sessions, JWTs, and OAuth 2.0 (with OIDC when you need identity). For how to validate JWT claims safely, see JWT deep dive. For Basic / Bearer / API key wire formats on REST, see REST authentication methods. For object-level authorization after login, see API security practices.

JWT vs Session vs OAuth compared
JWT vs Session vs OAuth compared

Skimmer: which model fits?

Sessions store state on the server and send a session ID cookie — instant revoke, natural for server-rendered apps. JWTs carry signed claims the client presents on each request — scalable across services, harder to revoke before expiry. OAuth 2.0 delegates authorization to an authorization server; OIDC adds identity (ID token) on top.

Decision order: classic web app that must kill sessions immediately → session cookie. Your APIs validate tokens across many instances without a shared session store → JWT (short-lived + refresh). Users grant a third party access or "Sign in with …" → OAuth 2.0 (+ OIDC for login). Production apps often combine them.

Quick reference

  • Session — server state; httpOnly cookie; instant logout.
  • JWT — signed claims; Bearer header or cookie; plan expiry + refresh.
  • OAuth — delegated access; Authorization Code + PKCE for public clients.
  • OIDC — identity layer on OAuth; validate iss, aud, exp on ID tokens.
  • Never put secrets in JWT payloads — Base64 is not encryption.

Remember this

Sessions need server-side state to revoke instantly; self-contained tokens trade that away for statelessness; OAuth/OIDC exists to delegate trust to a third party — pick by which trade-off you can live with.

JWT (JSON Web Token)

A JWT is three Base64URL parts: header (algorithm), payload (claims like sub and role), and signature (HMAC or RSA). After login the server signs a token; later requests send Authorization: Bearer <token>. Any service with the verification key can validate without a session lookup — useful for stateless APIs and microservices.

Revocation is the hard part. A JWT stays valid until exp unless you add a denylist or rotate keys aggressively. Prefer short-lived access tokens (minutes) plus refresh tokens. Payload is readable to anyone who has the token — treat claims as public. Full validation steps live in the JWT deep dive.

One logout: session dies immediately; JWT stays valid until exp
One logout: session dies immediately; JWT stays valid until exp

Quick reference

  • Best for: SPAs, mobile apps, microservices, API-to-API with shared verification.
  • Strengths: no shared session store; works across domains/services.
  • Weaknesses: hard to revoke early; token size grows with claims; XSS if stored in localStorage.
  • Prefer httpOnly Secure cookies for browser storage when same-site fits.
  • Sign with RS256/ES256 when many services verify; keep private keys only on the issuer.
JWT — Bearer call (success)
1// After login: access token issued with exp ~15m2const res = await fetch("https://api.example.com/v1/me", {3  headers: { Authorization: `Bearer ${accessToken}` },4});5if (!res.ok) throw new Error(`me ${res.status}`);6const me = await res.json(); // { id, email }
JWT — failure shapes
1if (res.status === 401) {2  // { "error": { "code": "invalid_token", "message": "jwt expired" } }3  // → refresh, then retry once; if refresh fails, re-login4}5if (res.status === 403) {6  // { "error": { "code": "forbidden", "message": "missing scope: orders:write" } }7  // → authenticated but not authorized for this action8}

Remember this

JWTs fit distributed APIs — keep payloads minimal and plan expiry, refresh, and verification.

Session-Based Auth

Session auth stores user state server-side — memory, Redis, or a database. On login the server creates a session and sets an httpOnly, Secure, SameSite cookie with the session ID. Each request sends the cookie; the server loads the session and user. Logout deletes the session — instant revocation.

This model fits traditional server-rendered apps and admin UIs. At scale you need a shared store (typically Redis) or sticky sessions. For a single-region web app with moderate traffic, sessions are often simpler than JWTs because the server owns all state.

Session: server stores state, cookie holds ID
Session: server stores state, cookie holds ID

Quick reference

  • Best for: server-rendered apps, dashboards, instant logout requirements.
  • Strengths: revoke by deleting server state; cookie is just an opaque ID.
  • Weaknesses: shared session store or stickiness; awkward for pure native clients.
  • Rotate session IDs after login to prevent fixation.
  • Use Redis (or equivalent) in production — not only in-process memory.
Session — login sets cookie
1// POST /login2app.post("/login", async (req, res) => {3  const user = await verifyPassword(req.body.email, req.body.password);4  if (!user) {5    return res.status(401).json({6      error: { code: "invalid_credentials", message: "Email or password incorrect" },7    });8  }9  const sid = await sessions.create({ userId: user.id });10  res.setHeader(11    "Set-Cookie",12    `sid=${sid}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400`13  );14  res.json({ ok: true });15});
Session — protected route + revoke
1app.get("/admin", async (req, res) => {2  const sid = parseCookie(req.headers.cookie).sid;3  const session = sid ? await sessions.get(sid) : null;4  if (!session) {5    return res.status(401).json({6      error: { code: "unauthorized", message: "No active session" },7    });8  }9  res.json({ userId: session.userId });10});11 12// POST /logout → delete session (instant revoke)13await sessions.delete(sid);14res.setHeader("Set-Cookie", "sid=; Max-Age=0; Path=/");

Remember this

Sessions are the secure default when the server can own state and instant revoke matters.

OAuth 2.0

OAuth 2.0 solves delegated authorization: a user grants a third-party app limited access without sharing their password. "Sign in with Google" redirects to the provider; after consent the app exchanges an authorization code for tokens. OAuth alone is not authentication — OpenID Connect (OIDC) adds an ID token that asserts who the user is.

Do not implement the full protocol from scratch; use a maintained library or IdP (Auth0, Keycloak, Cognito, NextAuth, and similar). For SPAs and mobile, use Authorization Code with PKCE — Implicit flow is obsolete. Store refresh tokens server-side when possible.

OAuth: third-party authorization server
OAuth: third-party authorization server

Quick reference

  • Best for: social login, third-party API access, enterprise SSO (OIDC/SAML).
  • Strengths: no password storage for social login; scoped delegated access.
  • Weaknesses: redirect complexity; provider dependency; easy to misconfigure.
  • Always Authorization Code + PKCE for public clients.
  • Validate ID tokens when OIDC is your authentication path.
OAuth — code exchange (server)
1// GET /callback?code=…&state=…2const tokenRes = await fetch("https://idp.example.com/oauth/token", {3  method: "POST",4  headers: { "Content-Type": "application/x-www-form-urlencoded" },5  body: new URLSearchParams({6    grant_type: "authorization_code",7    code,8    redirect_uri: REDIRECT_URI,9    client_id: CLIENT_ID,10    client_secret: CLIENT_SECRET, // confidential client only11    code_verifier: pkceVerifier,  // public clients (PKCE)12  }),13});14if (!tokenRes.ok) {15  // { "error": "invalid_grant", "error_description": "code expired" }16  throw new Error("token exchange failed");17}18const tokens = await tokenRes.json();19// { access_token, refresh_token?, id_token?, expires_in, token_type }
OAuth/OIDC — validate before trusting
1// Calling a resource server with the access token2const api = await fetch("https://api.example.com/v1/orders", {3  headers: { Authorization: `Bearer ${tokens.access_token}` },4});5if (api.status === 401) {6  // { "error": { "code": "invalid_token", "message": "token revoked or expired" } }7}8// If using OIDC for login: verify id_token signature, iss, aud, exp, nonce9// before creating a local session or minting your own API JWT

Remember this

Use OAuth for delegated access and OIDC for "Sign in with …" — not as a substitute for understanding sessions vs JWTs.

Key takeaway

Production apps combine models: OIDC for social login, short-lived JWTs for your APIs, refresh tokens held server-side; or sessions for a classic web app with optional OAuth for social login. Choose by client type, revoke needs, and who issues identity — not by which acronym is trendy.

Practice (25 min): For one product surface, write which model owns login, which credential hits your API, and how logout works. Then implement one happy path and one 401 body for either a session cookie check or a Bearer JWT check.

Share:

Related Articles

Jul 1, 2026 · 12 min read

A partner script scrapes your API with a leaked key. A mobile build ships a password in every header. A "Sign in with Gi

Read

Selecting the correct authorization flow is essential for securing modern applications. The OAuth 2.1 specification cons

Read

JSON Web Tokens are a common access-token format, but OAuth 2.0 does not require them: providers may issue opaque bearer

Read

Explore this topic

Keep learning

Follow a structured path or browse all courses to go deeper.