Skip to main content
Back to Blog
JWTOAuth2OIDCAuthenticationWeb SecurityAPI Security

JWT vs OAuth 2.0 vs OIDC: When to Use What in 2026

A practical 2026 guide comparing JWT, OAuth 2.0, and OIDC — their use cases, authentication flows, and security best practices for web, mobile, and single-page applications.

July 23, 202614 min readNiraj Kumar

Introduction

If you've built or debugged a login system in the last few years, you've almost certainly run into three acronyms that get thrown around interchangeably: JWT, OAuth 2.0, and OIDC. Despite being deeply related, they solve fundamentally different problems — and confusing them is one of the most common sources of security bugs in modern applications.

In 2026, with the proliferation of single-page applications (SPAs), native mobile apps, microservices, and third-party integrations, choosing the right combination of these technologies matters more than ever. Get it wrong, and you risk token leakage, session hijacking, or building an authentication system that simply doesn't scale.

This guide breaks down:

  • What JWT, OAuth 2.0, and OIDC actually are (and aren't)
  • How their flows work, with real code
  • When to use each one — for web apps, mobile apps, and SPAs
  • Security best practices and common mistakes to avoid

By the end, you'll have a clear mental model for architecting authentication and authorization in any modern application.


The Core Distinction (In One Sentence Each)

  • JWT (JSON Web Token) — A compact, self-contained token format for securely transmitting claims (data) between two parties.
  • OAuth 2.0 — An authorization framework that lets an application access resources on behalf of a user, without ever seeing the user's password.
  • OIDC (OpenID Connect) — An authentication layer built on top of OAuth 2.0 that lets an application verify a user's identity ("who is this person?").

A helpful analogy: think of a hotel.

  • OAuth 2.0 is the keycard system — it grants a specific level of access (your room, the gym, the pool) without giving you the master key.
  • OIDC is the front desk verifying your ID before issuing that keycard — it establishes who you are.
  • JWT is the physical keycard itself — a compact, tamper-evident object that encodes what you're allowed to access.

Let's unpack each one.


What Is JWT?

A JWT (JSON Web Token) is a URL-safe string format defined in RFC 7519 that represents a set of claims as a JSON object, digitally signed (and optionally encrypted).

A JWT has three parts, separated by dots:

header.payload.signature

Example decoded structure:

// Header
{
  "alg": "RS256",
  "typ": "JWT"
}

// Payload (claims)
{
  "sub": "user_12345",
  "name": "Asha Verma",
  "iat": 1753234800,
  "exp": 1753238400,
  "iss": "https://auth.example.com",
  "aud": "my-api"
}

The signature is computed over the encoded header and payload using a secret (HMAC) or a private key (RSA/ECDSA), which lets any recipient verify the token hasn't been tampered with.

Key Characteristics of JWT

  • Stateless — the server doesn't need to store session data; all necessary info lives in the token.
  • Self-contained — claims like user ID, roles, and expiration are embedded directly.
  • Signed, not necessarily encrypted — anyone can read a JWT's payload (it's just Base64URL-encoded JSON), but they can't modify it without invalidating the signature.

⚠️ Important: JWT is just a format. It is not an authentication protocol by itself. You can use JWTs for session tokens, API keys, OAuth access tokens, or OIDC ID tokens — the format is reused everywhere, which is exactly why people conflate it with OAuth and OIDC.

Verifying a JWT (Node.js Example)

const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json'
});

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    const signingKey = key.getPublicKey();
    callback(null, signingKey);
  });
}

function verifyToken(token) {
  return new Promise((resolve, reject) => {
    jwt.verify(
      token,
      getKey,
      {
        algorithms: ['RS256'],
        issuer: 'https://auth.example.com',
        audience: 'my-api'
      },
      (err, decoded) => {
        if (err) return reject(err);
        resolve(decoded);
      }
    );
  });
}

Notice the explicit checks: algorithms, issuer, and audience. Skipping any of these is a common — and dangerous — mistake (more on that later).


What Is OAuth 2.0?

OAuth 2.0 (RFC 6749) is an authorization framework that allows a third-party application to obtain limited access to a user's resources without exposing their credentials.

Think of "Sign in with Google to let App X read your Google Calendar." OAuth 2.0 is what makes that possible — App X never sees your Google password; it only receives a scoped access token.

Key Roles in OAuth 2.0

RoleDescription
Resource OwnerThe user who owns the data
ClientThe application requesting access
Authorization ServerIssues tokens after authenticating the user
Resource ServerHosts the protected data/API

The Modern Standard Flow: Authorization Code + PKCE

By 2026, the Authorization Code Flow with PKCE (Proof Key for Code Exchange) is the universally recommended flow for all client types — web, mobile, and SPA — as formalized in the OAuth 2.1 consolidation effort. The old Implicit Flow is deprecated because it exposes access tokens directly in the browser URL fragment, making them vulnerable to leakage via browser history, referrer headers, and logs.

Here's how Authorization Code + PKCE works:

  1. The client generates a random code_verifier and derives a code_challenge (SHA-256 hash of it).
  2. The client redirects the user to the authorization server with the code_challenge.
  3. The user authenticates and consents.
  4. The authorization server redirects back with a one-time authorization_code.
  5. The client exchanges the code plus the original code_verifier for an access token.
  6. The authorization server verifies the code_verifier matches the earlier code_challenge before issuing tokens.
// Step 1: Generate PKCE values
function generatePKCE() {
  const codeVerifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32)));
  const codeChallenge = base64URLEncode(sha256(codeVerifier));
  return { codeVerifier, codeChallenge };
}

// Step 2: Redirect to authorization server
const authUrl = `https://auth.example.com/authorize?` +
  `response_type=code&` +
  `client_id=abc123&` +
  `redirect_uri=https://app.example.com/callback&` +
  `scope=openid profile email&` +
  `code_challenge=${codeChallenge}&` +
  `code_challenge_method=S256&` +
  `state=${randomState}`;

// Step 5: Exchange code for tokens
const response = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: 'https://app.example.com/callback',
    client_id: 'abc123',
    code_verifier: codeVerifier
  })
});

PKCE was originally designed for public clients (mobile/SPA) that can't safely store a client secret, but it's now recommended for all clients as defense-in-depth against authorization code interception attacks.

Other OAuth 2.0 Grant Types (2026 Status)

Grant TypeUse Case2026 Recommendation
Authorization Code + PKCEWeb, mobile, SPA✅ Recommended for all
Client CredentialsMachine-to-machine (M2M)✅ Recommended
Device Authorization GrantSmart TVs, CLI tools✅ Recommended
Refresh TokenLong-lived sessions✅ Recommended (with rotation)
Implicit FlowSPAs (legacy)❌ Deprecated — do not use
Resource Owner Password CredentialsFirst-party legacy apps❌ Deprecated — avoid

What Is OIDC (OpenID Connect)?

OpenID Connect is a thin identity layer built directly on top of OAuth 2.0. While OAuth 2.0 answers "What can this app access?", OIDC answers "Who is this user?"

OIDC adds:

  • A standardized ID Token — a JWT containing identity claims (sub, email, name, iat, exp, etc.)
  • A standardized /userinfo endpoint to fetch additional profile data
  • A scope=openid requirement that signals to the authorization server that identity information should be returned
  • Discovery documents (/.well-known/openid-configuration) so clients can auto-configure endpoints and keys

ID Token vs Access Token — A Crucial Distinction

This is one of the most misunderstood parts of the entire stack:

ID TokenAccess Token
PurposeProves user identity to the clientGrants access to a resource server/API
AudienceThe client applicationThe API/resource server
FormatAlways a JWTOften a JWT, sometimes opaque
Who should validate itThe client (your app)The API being called
Should APIs accept it as auth?❌ Never✅ Yes

🚫 Common bug: Sending the ID token to your backend API as a Bearer token. The ID token was minted for your app, not for the API — its audience (aud) claim won't match, and using it this way breaks the security model entirely.

Example ID Token Payload

{
  "iss": "https://auth.example.com",
  "sub": "user_12345",
  "aud": "spa-client-id",
  "exp": 1753238400,
  "iat": 1753234800,
  "email": "asha@example.com",
  "email_verified": true,
  "name": "Asha Verma"
}

Putting It Together: A Real-World Login Flow

Imagine a SaaS dashboard app that lets users "Sign in with Google" and then calls its own backend API.

  1. OIDC handles authentication — the app redirects to Google's authorization endpoint with scope=openid email profile.
  2. Google authenticates the user and returns an ID token (proves identity) and an access token (if the app also requested Google API scopes).
  3. The app decodes the ID token to display "Welcome, Asha" and creates an app session.
  4. When the SPA calls its own backend API, it uses its own access token (often a JWT issued by the app's authorization server), not Google's token.
  5. The backend API validates that JWT's signature, issuer, audience, and expiration on every request.

This is the layered architecture in a nutshell:

OIDC  → authenticates the user (who are you?)
OAuth → authorizes access (what can you do?)
JWT   → the token format carrying both of the above

Web, Mobile, and SPA: When to Use What

Traditional Server-Rendered Web Apps

  • Best fit: Authorization Code Flow (with a confidential client secret) + OIDC for login.
  • Store tokens server-side in an encrypted, HttpOnly, Secure, SameSite=Lax session cookie.
  • The browser never directly handles raw access/ID tokens.
// Express + a library like passport or openid-client
app.get('/callback', async (req, res) => {
  const tokenSet = await client.callback(redirectUri, params, { code_verifier });
  req.session.user = tokenSet.claims(); // server-side session
  res.redirect('/dashboard');
});

Single-Page Applications (SPAs)

  • Best fit: Authorization Code Flow with PKCE, using a Backend-for-Frontend (BFF) pattern where possible.
  • Avoid storing access/refresh tokens in localStorage — it's vulnerable to XSS-based theft.
  • The 2026 best practice is the BFF pattern: the SPA talks to its own lightweight backend, which holds tokens in a secure, HttpOnly cookie session and proxies API calls. The browser never sees raw tokens at all.
  • If a BFF isn't feasible, use in-memory token storage (JS variables, not localStorage) combined with silent refresh via refresh token rotation.

Native Mobile Apps

  • Best fit: Authorization Code Flow with PKCE, using the system browser (via ASWebAuthenticationSession on iOS or Chrome Custom Tabs on Android) — never an embedded WebView, which can be manipulated to intercept credentials.
  • Use App-Claimed HTTPS redirect URIs (Universal Links / App Links) instead of custom URI schemes where possible, since custom schemes can be hijacked by malicious apps.
  • Store tokens in the platform's secure storage: Keychain (iOS) or Keystore/EncryptedSharedPreferences (Android).

Machine-to-Machine (APIs, Microservices, CI/CD)

  • Best fit: OAuth 2.0 Client Credentials Grant — no user is involved, so OIDC isn't relevant here.
  • Tokens are typically short-lived JWTs signed by the authorization server, validated by each downstream service.

Security Considerations for 2026

The threat landscape has evolved, and so have the mitigations:

  • PKCE is mandatory, not optional — even confidential clients should use it as defense-in-depth.
  • Refresh Token Rotation — every time a refresh token is used, issue a new one and invalidate the old, so a stolen refresh token has a short window of usefulness (this is called refresh token reuse detection).
  • DPoP (Demonstrating Proof of Possession) — an emerging standard (RFC 9449) that cryptographically binds a token to the client possessing a private key, mitigating stolen-token replay even over TLS.
  • Short-lived access tokens — 5–15 minutes is common, paired with longer-lived, rotated refresh tokens.
  • aud and iss validation — always verify a token was issued for your service by a trusted issuer, not just that its signature is valid.
  • Algorithm allow-listing — explicitly specify accepted signing algorithms (RS256, ES256) when verifying JWTs. Never accept alg: none, and never let the algorithm be dictated by the token's own header without a server-side allow-list — this prevents the classic "algorithm confusion" attack.
  • FAPI 2.0 (Financial-grade API) — for high-assurance use cases (banking, healthcare), consider adopting FAPI 2.0 profiles, which layer stricter requirements onto OAuth/OIDC (mandatory PKCE, sender-constrained tokens, stricter redirect URI matching).

Best Practices Checklist

  • ✅ Use OIDC for authentication, OAuth 2.0 scopes for authorization — don't overload one token with both roles unnecessarily.
  • ✅ Always use Authorization Code Flow + PKCE, regardless of client type.
  • ✅ Validate JWTs fully: signature, iss, aud, exp, and nbf (not-before) claims.
  • ✅ Keep access tokens short-lived; rely on refresh token rotation for longevity.
  • ✅ Use a BFF pattern for SPAs whenever feasible to keep tokens out of the browser's JS context.
  • ✅ Rotate signing keys periodically and expose them via a JWKS endpoint.
  • ✅ Log and monitor for refresh token reuse (a strong signal of token theft).
  • ✅ Use HTTPS everywhere — tokens over plaintext HTTP are catastrophic.

Common Mistakes to Avoid

  • Treating JWT as a security protocol. JWT is just a data format — signing doesn't mean encryption, and a valid signature doesn't mean the claims are trustworthy for your context.
  • Using the Implicit Flow in 2026. It's deprecated for good reason — tokens leak via browser history and referrer headers.
  • Storing tokens in localStorage. This is directly exploitable by any successful XSS attack. Prefer HttpOnly cookies or in-memory storage.
  • Sending the ID token to your API instead of the access token. The ID token's audience is the client app, not your backend.
  • Skipping aud/iss validation. A signature check alone doesn't confirm the token was meant for your service.
  • Using embedded WebViews for OAuth on mobile. Always use the system browser or an in-app secure browser tab.
  • Never expiring or rotating tokens. Long-lived, non-rotating tokens massively increase the blast radius of any leak.

🚀 Pro Tips

  • Use a managed identity provider (Auth0, Okta, AWS Cognito, Keycloak, Ory) instead of hand-rolling OAuth/OIDC unless you have a very specific reason not to — the edge cases here are numerous and security-critical.
  • Adopt the BFF pattern for SPAs in 2026 — it's rapidly becoming the default recommendation over pure browser-side token handling.
  • Test token expiry paths explicitly — many production incidents come from apps that never properly tested what happens when a refresh token expires or is revoked mid-session.
  • Use state and nonce parameters in every OAuth/OIDC flow to prevent CSRF and replay attacks — don't skip these even in "simple" implementations.
  • Prefer asymmetric signing (RS256/ES256) over HMAC (HS256) for JWTs whenever multiple services need to verify tokens — HMAC requires sharing a secret with every verifier, increasing risk.

📌 Key Takeaways

  • JWT is a token format — useful, but not inherently an authentication or authorization protocol.
  • OAuth 2.0 governs authorization — what an app can access on a user's behalf.
  • OIDC governs authentication — confirming a user's identity, built on top of OAuth 2.0.
  • In 2026, Authorization Code Flow + PKCE is the universal standard across web, mobile, and SPA clients.
  • For SPAs, the BFF pattern is increasingly the recommended architecture to keep tokens out of client-side JavaScript.
  • Always validate JWTs fully (signature, iss, aud, exp) and never conflate ID tokens with access tokens.

Conclusion

JWT, OAuth 2.0, and OIDC aren't competing technologies — they're complementary layers in a well-designed authentication and authorization architecture. JWT gives you a compact, verifiable token format. OAuth 2.0 gives you a standardized way to grant scoped access. OIDC gives you a reliable way to establish user identity on top of that.

The mistakes that cause security incidents rarely come from these standards themselves — they come from misapplying them: using the wrong token in the wrong place, storing tokens insecurely, or skipping validation steps that seem "optional" until they're not. As you design authentication for your next web, mobile, or SPA project, keep the mental model simple: OIDC for who, OAuth for what, JWT for how.

Get that right, and the rest of the implementation — flows, storage, validation — follows naturally.


References

All Articles
JWTOAuth2OIDCAuthenticationWeb SecurityAPI Security

Written by

Niraj Kumar

Software Developer — building scalable systems for businesses.