JWT vs OAuth2 vs OpenID Connect: Simple Explanation for Developers
TL;DR
- JWT (JSON Web Token) is a token format — a compact, URL-safe way to represent claims that can be signed (and optionally encrypted).
- OAuth 2.0 is an authorization framework for delegated access (how a client gets permission to access a resource).
- OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that enables authentication (verifying who a user is) and returns an ID Token (usually a JWT).
- They are complementary: OAuth2 handles authorization, OIDC adds authentication and ID tokens, and JWT is one common token representation used by both.
This article gives a developer-friendly, in-depth view: history, concepts, flows, security best practices, practical examples and when to use each.
Contents
- Background & history (standards)
- Core concepts & actors
- JWT deep dive (structure, signing, validation)
- OAuth 2.0 deep dive (flows, endpoints, tokens)
- OpenID Connect deep dive (ID token, discovery, userinfo)
- Comparing JWT, OAuth2, OIDC (short decision guide)
- Common flows and practical examples (code snippets)
- Security pitfalls & best practices
- Deployment/architecture patterns (JWT vs opaque tokens, introspection, gateways)
- Popular libraries and providers
- Current state & future directions
- FAQs and quick recommendations
Background & history (standards)
- JWT: defined in RFC 7519 (2015). Part of a family of JSON-based identity specs (JWS, JWE, JWK).
- OAuth 2.0: defined in RFC 6749 (2012). A widely used framework for delegated authorization.
- OpenID Connect: published 2014 (built on OAuth2). Core spec adds authentication semantics and ID Tokens.
Why confusion? Because these technologies often appear together (e.g., an OAuth2 server emits JWT access tokens and OIDC emits JWT ID tokens). But they answer different questions:
- Who is the user? (authentication) — OIDC
- Can this client do X on behalf of the user? (authorization) — OAuth2
- How do we encode claims or tokens? (token format) — JWT
Core concepts & actors
Common actors (OAuth2/OIDC terminology):
- Resource Owner: the user or entity owning the resource.
- Client: application wanting access (web app, mobile app, backend).
- Authorization Server: issues tokens and performs authentication/consent.
- Resource Server (API): consumes and validates access tokens to permit access.
Key terms:
- Access token: used to access protected resources (opaque or JWT).
- Refresh token: used to obtain new access tokens without re-authenticating the user.
- ID token (OIDC): token containing user identity claims (usually a JWT).
- Scope: requested/allowed permissions (e.g., profile email read).
- Grant/Flow: method a client uses to obtain tokens (authorization code, client credentials, etc.).
- JWK: JSON Web Key; public key format used to expose verification keys.
JWT deep dive
What is a JWT?
- Compact, URL-safe token composed of 3 parts: header, payload (claims), signature.
- Encoded as Base64URL(header) + "." + Base64URL(payload) + "." + Base64URL(signature).
Header example:
1{
2 "alg": "RS256",
3 "typ": "JWT",
4 "kid": "abc123"
5}Payload (claims) example:
1{
2 "iss": "https://auth.example.com",
3 "sub": "user-123",
4 "aud": "api.example.com",
5 "exp": 1618887999,
6 "iat": 1618884399,
7 "scope": "read:messages"
8}Standard claims:
- iss (issuer), sub (subject), aud (audience), exp (expiration), nbf (not before), iat (issued at), jti (unique id).
Signing and encryption:
- JWS (signed): common algorithms HS256 (HMAC) and RS256/ES256 (asymmetric). RS/ES recommended for distributed verification (no shared secret).
- JWE (encrypted): JWT can be encrypted to hide claims.
Advantages
- Self-contained: resource servers can validate without contacting the auth server (if they have verification keys).
- Compact and widely supported.
Disadvantages and caveats
- Revocation is hard for long-lived JWTs (stateless). Use short TTLs and/or revocation strategies (blacklists or introspection).
- Large JWTs increase request size (cookies/headers).
- Inappropriate audience/issuer checks are common bugs.
Example: create & verify JWT (Node.js)
1// npm install jsonwebtoken jwks-rsa
2const jwt = require('jsonwebtoken');
3const fs = require('fs');
4
5// Signing with RSA private key (server)
6const privateKey = fs.readFileSync('./private.pem');
7const token = jwt.sign({ sub: 'user-123', scope: 'read' }, privateKey, {
8 algorithm: 'RS256',
9 expiresIn: '1h',
10 issuer: 'https://auth.example.com',
11 audience: 'https://api.example.com'
12});
13console.log(token);
14
15// Verification (resource server)
16const publicKey = fs.readFileSync('./public.pem');
17try {
18 const payload = jwt.verify(token, publicKey, {
19 algorithms: ['RS256'],
20 issuer: 'https://auth.example.com',
21 audience: 'https://api.example.com'
22 });
23 console.log(payload);
24} catch (err) {
25 console.error('Invalid token', err);
26}OAuth 2.0 deep dive
Purpose: delegated authorization — let a third-party client access resources on behalf of the user.
Primary endpoints:
- Authorization endpoint: user-agent redirect for user consent and auth.
- Token endpoint: exchange authorization code (or credentials) for tokens.
- (Optional) Revocation and introspection endpoints.
Main grant types (flows)
- Authorization Code (with PKCE): recommended for web & mobile apps. Server-side or public clients get a code then exchange it for tokens.
- Implicit: older pattern for single-page apps returning tokens directly; largely deprecated due to security concerns.
- Client Credentials: machine-to-machine; client acts on its own behalf (no user).
- Resource Owner Password Credentials: deprecated/avoid — app collects user credentials directly.
- Device Authorization (device code): for input-constrained devices.
- Refresh Token: refresh access tokens without re-prompting the user.
Important: PKCE (Proof Key for Code Exchange)
- Protects authorization code flow for public clients (mobile, SPAs) from code interception.
- Client generates code_verifier and code_challenge sent to auth endpoint; token exchange requires the code_verifier.
Token types:
- Access tokens: may be JWT or opaque strings — used at resource servers.
- Refresh tokens: usually opaque and long-lived; used to get new access tokens.
Sequence: Authorization Code flow (simplified)
- Client redirects user to Authorization Endpoint with client_id, redirect_uri, scope, state, code_challenge.
- User authenticates and consents.
- Auth server redirects back with code and state.
- Client sends code + code_verifier to Token Endpoint.
- Server returns access_token (and maybe refresh_token).
Curl example: exchange code for tokens (sketch)
curl -X POST https://auth.example.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https://app.example.com/cb&client_id=CLIENT_ID&code_verifier=CODE_VERIFIER"OpenID Connect deep dive
What OIDC adds:
- ID Token: JWT that contains identity claims about the authenticated user (sub, iss, aud, exp, iat, nonce...). It's intended for the client (Relying Party).
- Well-Known Discovery: /.well-known/openid-configuration gives endpoints, supported features, JWK URI.
- UserInfo Endpoint: returns additional claims about the user (profile, email) using the access_token.
- Standard scopes: openid (required to do OIDC), profile, email, address, phone, offline_access (request refresh token).
- Nonce: used to mitigate replay attacks against the ID token — client sends nonce during authorization request and validates it matches ID token claim.
Typical OIDC flow:
- Same as OAuth2 Authorization Code flow, but include scope=openid (and optionally profile,email).
- Token response includes id_token (JWT) + access_token.
- Client validates id_token (signature, issuer, audience, exp, nonce).
ID token example payload:
1{
2 "iss": "https://auth.example.com",
3 "sub": "user-123",
4 "aud": "client-abc",
5 "exp": 1618887999,
6 "iat": 1618884399,
7 "nonce": "n-0S6_WzA2Mj",
8 "email": "[email protected]",
9 "email_verified": true
10}Discovery example (curl)
curl https://auth.example.com/.well-known/openid-configuration
# returns JSON with issuer, authorization_endpoint, token_endpoint, jwks_uri, userinfo_endpoint...Key OIDC validations:
- Validate signature (using JWKs from jwks_uri).
- Validate iss, aud, exp, iat.
- Verify nonce (for code flow with id_token).
- If access token is used at userinfo endpoint, it must be sent securely.
Comparing JWT vs OAuth2 vs OIDC — short decision guide
- Need to authenticate users (single sign-on, identity)? Use OpenID Connect.
- Need delegated authorization to APIs (access on behalf of user)? Use OAuth 2.0.
- Need to choose how to format tokens? JWT is one format (but OAuth2 is agnostic).
- Want stateless tokens that can be self-validated by resource servers? JWT (with signatures).
- Want revocable/opaque tokens (server-side control)? Opaque tokens + introspection.
Comparison table (simplified):
- Purpose:
- JWT — token format
- OAuth2 — authorization framework
- OIDC — authentication layer on OAuth2
- Typical token:
- JWT — signed JSON
- OAuth2 — access_token (opaque or JWT)
- OIDC — id_token (JWT), access_token (OAuth2)
- Used for:
- JWT — carry claims; stateless validation
- OAuth2 — grant access, microservices auth
- OIDC — authenticate users, SSO
- Revocation:
- JWT — harder (stateless)
- OAuth2 opaque — easier via revocation/introspection
- OIDC — same as OAuth2 for access tokens; ID tokens are ephemeral
Common flows and practical examples
- Web application (server-side) login and API access — Recommended:
- Use OAuth2 Authorization Code + PKCE (or standard code flow for confidential clients).
- Use OIDC (scope=openid) to get ID token and user info for authentication.
- Access token granted is used to call APIs.
- Single-page application (SPA):
- Use Authorization Code with PKCE (do NOT use implicit).
- Store tokens securely (prefer HttpOnly cookie or short-lived access token + refresh via backend).
- Beware of XSS — avoid localStorage for long-lived tokens.
- Machine-to-machine:
- Use Client Credentials grant (OAuth2) — no user, just client authentication.
- Typically return an access token (often JWT).
- Mobile apps:
- Authorization Code with PKCE.
- Use system browser or secure webview; avoid embedded webviews.
Sample: Verify OIDC id_token in Node.js using jwks-rsa
1const jwt = require('jsonwebtoken');
2const jwksClient = require('jwks-rsa');
3
4const client = jwksClient({
5 jwksUri: 'https://auth.example.com/.well-known/jwks.json'
6});
7
8function getKey(header, callback){
9 client.getSigningKey(header.kid, function(err, key) {
10 if (err) return callback(err);
11 const signingKey = key.publicKey || key.rsaPublicKey;
12 callback(null, signingKey);
13 });
14}
15
16const idToken = 'eyJ...';
17jwt.verify(idToken, getKey, {
18 algorithms: ['RS256'],
19 audience: 'client-abc',
20 issuer: 'https://auth.example.com'
21}, function(err, payload) {
22 if (err) console.error('Invalid ID token', err);
23 else console.log('ID Token payload', payload);
24});Security pitfalls & best practices
- Use TLS everywhere (HTTPS mandatory).
- Short-lived access tokens:
- Make access tokens short-lived (minutes to an hour).
- Use refresh tokens (with cautious handling).
- Use PKCE for public clients (mobile, SPA).
- Prefer asymmetric signing (RS256/ES256) so resource servers only need public keys.
- Validate everything:
- Verify signature
- Check iss (issuer), aud (audience), exp (expiration), nbf/i-at
- For OIDC: validate nonce (prevent replay)
- Token storage in browser:
- Avoid localStorage for sensitive refresh tokens; use secure httpOnly SameSite cookies or a backend token exchange.
- If using cookies, set Secure, HttpOnly, SameSite attributes.
- Revocation and logout:
- Provide revocation endpoint and implement log-out flows.
- Key rotation:
- Use JWKs and rotate keys; clients should fetch JWKs (jwks_uri).
- Avoid large JWTs:
- Keep claims minimal; store user profile on resource server if needed.
- Audiences & scope:
- Check that the token was intended for your API (aud claim).
- Check scopes to enforce fine-grained access.
- Use DPoP/mTLS/Token Binding for enhanced protection in modern scenarios (mitigates token replay).
Common vulnerabilities:
- Accepting unsigned (alg=none) tokens: reject entirely.
- Using HS256 with leaked secret across services: prefer RS256.
- Not checking aud/iss leading to accepting tokens from wrong issuer.
- Storing refresh tokens insecurely leading to theft.
- CSRF in cookie-based flows: protect with same-site or CSRF tokens.
Deployment patterns: opaque tokens vs JWT vs introspection
- Self-contained JWTs:
- Pros: resource servers validate locally (no network call).
- Cons: harder to revoke; ensure short TTL and key rotation.
- Opaque tokens (random strings):
- Pros: central control; can revoke instantly via token store; smaller tokens.
- Cons: resource servers call introspection endpoint (network latency).
- Hybrid: Use JWT for access token, but support introspection for revocation checks or when fine-grained dynamic policies are needed.
API Gateway pattern:
- Gateway validates tokens, enforces policies, forwards validated identity to backend (e.g., as HTTP header).
- Gateways reduce per-service validation repetition and centralize security.
Token introspection (RFC 7662)
- Authorization server exposes introspection endpoint to let resource server validate opaque tokens and learn associated metadata (active, scope, client_id, exp, sub).
Token exchange (RFC 8693)
- Exchange one token for another (e.g., exchange an external token for a local token with reduced privileges).
Libraries and providers
Server libraries (examples):
- Node.js: passport.js, openid-client, simple-oauth2
- Python: oauthlib, authlib, PyJWT
- Java: Spring Security OAuth, Nimbus JOSE+JWT
- Go: golang-jwt/jwt, go-oidc
Identity providers (IDaaS):
- Auth0, Okta, AWS Cognito, Azure AD, Google Identity Platform, Keycloak (open source), ForgeRock, Ping Identity.
Current state & future directions
- OAuth 2.1: consolidating best practices (PKCE for all clients, removal of implicit flow).
- DPoP (Demonstration of Proof-of-Possession): prevent token replay by binding token usage to a key.
- mTLS: client authentication using TLS certificates.
- JWT Best Practices: guidance to restrict claims, avoid long-lived tokens, prefer asymmetric crypto.
- Continuous Access Evaluation (CAE): frameworks to revoke access in near real-time (e.g., session changes).
- Token Exchange and federated identity: more use of token transformation and federation across systems.
Expect more standardized protections in browser-based apps and mobile, and better guidelines for refresh token storage in SPAs.
FAQs & quick developer guidance
Q: Is JWT an authentication protocol? A: No. JWT is a token format. Authentication protocol is OIDC (which issues ID tokens, typically JWTs).
Q: My API returns JWT access tokens. Is it safe to validate locally? A: Yes, but ensure you validate signature, iss, aud and expiration and have short TTLs. Consider token revocation intervals or introspection if you need immediate revocation.
Q: Should I store tokens in localStorage? A: Avoid it for refresh tokens due to XSS risk. HttpOnly, Secure cookies with SameSite are usually safer — but then protect against CSRF.
Q: Use OAuth2 or OIDC for web app login? A: Use OIDC (it uses OAuth2) for authentication + SSO, and use the access token for API authorization.
Q: Which algorithms for signing? A: Prefer asymmetric (RS256/ES256). HS256 requires a shared secret between issuer and validators which is riskier in distributed environments.
Conclusion — Simple rules for developers
- Use OAuth 2.0 for delegated authorization.
- Use OpenID Connect when you need to authenticate users / get identity (use scope=openid).
- Treat JWT as a useful token format but not a security silver bullet — validate signatures, claims, and use short lifetimes.
- For SPAs and mobile apps, use Authorization Code + PKCE.
- Prefer asymmetric signing and JWKs for key distribution.
- Carefully choose between opaque tokens (easy revocation) and JWTs (local validation) based on your revocation/latency needs.
Appendix: Quick cheat sheet of flows
- Authorization Code + PKCE: recommended for web, mobile, SPA (user login + refresh).
- Client Credentials: machine-to-machine (no user).
- Device Code: TV/IoT devices with limited input.
- Refresh Token: used to refresh access tokens (keep secure).
- Implicit: deprecated; avoid.
If you want, I can:
- Provide a step-by-step example implementing an Authorization Code + PKCE flow (client + auth server) with sample code.
- Show a complete Express middleware for JWT validation and role-checking.
- Create a checklist you can use while evaluating identity providers.