OAuth & JWT Authentication Cheat Sheet
Explains OAuth 2.0 grant types, the Authorization Code plus PKCE flow, and JWT structure, claims, and verification for securing APIs.
OAuth 2.0 Grant Types
Which flow to use for which kind of client.
- Authorization Code- Most secure flow for server-side apps; exchanges a code for tokens
- Authorization Code + PKCE- Required for public clients (SPAs, mobile apps) to prevent code interception
- Client Credentials- Machine-to-machine auth; app authenticates as itself, no user involved
- Refresh Token- Exchanges a long-lived refresh token for a new access token without re-login
- Device Code- For input-constrained devices (smart TVs); user authorizes on a second device
- Implicit (deprecated)- Returned tokens directly in the URL fragment; insecure, replaced by PKCE
Authorization Code Flow (PKCE)
Generating a code verifier/challenge and exchanging the code for tokens.
# 1. Generate PKCE verifier and challengeCODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=+/')CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | base64 | tr -d '=+/')# 2. Redirect the user to the authorization endpoint# https://auth.example.com/authorize?# response_type=code&client_id=abc123&redirect_uri=https://app.com/callback# &scope=openid%20profile&state=xyz&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256# 3. Exchange the returned ?code=... for tokenscurl -X POST https://auth.example.com/token \ -d grant_type=authorization_code \ -d code=AUTH_CODE_FROM_REDIRECT \ -d redirect_uri=https://app.com/callback \ -d client_id=abc123 \ -d code_verifier=$CODE_VERIFIER# Response# {"access_token":"eyJ...","refresh_token":"...","expires_in":3600,"token_type":"Bearer"}
JWT Structure & Verification
Signing and verifying a JSON Web Token with a shared secret.
// A JWT has 3 base64url parts: header.payload.signature// eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4f...const jwt = require('jsonwebtoken');// Sign (server issues token)const token = jwt.sign( { sub: 'user123', role: 'admin' }, process.env.JWT_SECRET, { expiresIn: '1h', issuer: 'api.example.com' });// Verify (server validates incoming token)try { const payload = jwt.verify(token, process.env.JWT_SECRET, { issuer: 'api.example.com', }); console.log(payload.sub);} catch (err) { // TokenExpiredError, JsonWebTokenError, etc. console.error('Invalid token:', err.message);}
Standard JWT Claims
Registered claim names defined by RFC 7519.
- iss- Issuer — identifies who created and signed the token
- sub- Subject — the user/entity the token represents
- aud- Audience — intended recipient(s) of the token
- exp- Expiration time (Unix timestamp); token is invalid after this
- iat- Issued-at time
- nbf- Not-before time; token is invalid until this timestamp
- jti- JWT ID — unique identifier, useful for revocation lists
Verifying with a JWKS Endpoint (Key Rotation)
Fetching the issuer's public keys by kid instead of hardcoding a secret.
const { createRemoteJWKSet, jwtVerify } = require('jose');// Cached, auto-refreshing set of the issuer's public signing keysconst JWKS = createRemoteJWKSet( new URL('https://auth.example.com/.well-known/jwks.json'));async function verifyAccessToken(token) { const { payload, protectedHeader } = await jwtVerify(token, JWKS, { issuer: 'https://auth.example.com', audience: 'api://my-api', }); // protectedHeader.kid tells you which key in the JWKS signed this token, // so the issuer can rotate keys without invalidating already-issued tokens return payload;}// JWKS document shape:// { "keys": [ { "kty": "RSA", "kid": "2026-01", "n": "...", "e": "AQAB", "alg": "RS256", "use": "sig" } ] }
Refresh Token Rotation & Reuse Detection
Issuing a new refresh token on every use and revoking the family if a stolen token is replayed.
async function refresh(oldRefreshToken) { const record = await db.refreshTokens.findByToken(oldRefreshToken); if (!record) throw new Error('unknown refresh token'); if (record.revoked) { // The token was already used once before -> this is a replay. // Assume the whole token family is compromised and kill every // descendant issued from the same original login. await db.refreshTokens.revokeFamily(record.familyId); throw new Error('refresh token reuse detected — session family revoked'); } // Rotate: invalidate the old token, mint a new one in the same family await db.refreshTokens.revoke(record.id); const newRefreshToken = generateOpaqueToken(); await db.refreshTokens.create({ token: newRefreshToken, familyId: record.familyId, userId: record.userId, }); const accessToken = signAccessToken({ sub: record.userId }); return { accessToken, refreshToken: newRefreshToken };}
Token Introspection & Revocation (RFC 7662 / 7009)
Server-side calls to check or kill an opaque or JWT token before its natural expiry.
# Introspection: ask the auth server whether a token is still valid# (needed for opaque tokens, or JWTs you want to check against a revocation list)curl -X POST https://auth.example.com/introspect \ -u client_id:client_secret \ -d token=eyJhbGciOiJSUzI1NiJ9...# Response# {"active": true, "scope": "read write", "sub": "user123", "exp": 1739980800}# Revocation: invalidate a token immediately (e.g. on logout)curl -X POST https://auth.example.com/revoke \ -u client_id:client_secret \ -d token=eyJhbGciOiJSUzI1NiJ9... \ -d token_type_hint=refresh_token# Because JWT access tokens are self-contained, revoking them before exp# requires either a short TTL + refresh, or checking a deny-list keyed on 'jti'
OpenID Connect vs Plain OAuth 2.0
OIDC is an identity layer built on top of OAuth 2.0 — the two tokens serve different purposes.
- OAuth 2.0- An authorization framework; grants an access token to call APIs on the user's behalf
- OIDC ID token- A JWT proving who the user is, meant for the client app itself, not for calling APIs
- OIDC access token- Same access token as OAuth; opaque or JWT, sent to resource servers, never inspected by the client
- userinfo endpoint- GET /userinfo with the access token returns the profile claims (name, email, etc.)
- openid scope- Requesting 'openid' in the scope is what turns an OAuth flow into an OIDC flow
- nonce parameter- Bound into the ID token to prevent replay of a stolen authorization response
JWT Implementation Pitfalls
Common mistakes that turn JWTs into a vulnerability instead of a defense.
- alg:none attack- Never trust the 'alg' header from the token; pin the expected algorithm server-side
- RS256/HS256 confusion- If an attacker can submit alg:HS256 and the server reuses its RSA public key as the HMAC secret, they can forge tokens — reject algorithm switching
- Missing audience check- A token valid for one API can be replayed against another if 'aud' isn't verified
- No expiry enforcement- Always check 'exp' server-side even if the library claims to — misconfigured verify() calls can skip it
- Oversized tokens- Don't cram full user profiles into the JWT payload; it's sent on every request and is only base64, not encrypted
- No revocation path- Pure stateless JWTs can't be revoked early — pair long-lived sessions with a short access-token TTL
Never store JWTs containing sensitive claims in localStorage for browser apps — it's readable by any injected script (XSS). Prefer httpOnly, Secure, SameSite cookies for the access and refresh token pair.