Backend Engineering / Authentication
OAuth2 / OIDC in Plain English: JWT vs Reference Tokens + Refresh Tokens (Without Confusion)
A production-auth mental model you can use immediately—without drowning in specs.
Written by





Production APIs Playbook — Part 2 of 5
If you’ve ever shipped “JWT auth” and still felt unsure, you’re not alone. Most developers learn tokens as a coding trick. Production teams treat auth as a system: identity, sessions, permissions, and safe rotation.
This post explains OAuth2 and OpenID Connect (OIDC) in a way you can actually use, then clarifies the biggest confusion: when to use JWT access tokens vs opaque (reference) tokens, and how refresh tokens fit in.
What OAuth2 and OIDC actually do
OAuth2 is an authorization framework. It answers: “How can an app get permission to call an API on behalf of a user?”
OIDC is an identity layer on top of OAuth2. It answers: “Who is the user?” and provides standardized login (authentication).
A simple mental model:
OAuth2 = permission to access resources (scopes)
OIDC = login + identity claims (who the user is)
In real products, you almost always use them together.
The 4 pieces you should know (no fluff)
Resource Owner: the user
Client: your app (web/mobile/backend)
Authorization Server: the identity provider (IdP) that issues tokens
Resource Server: your API that accepts tokens
Your API should not “invent identity.” It should validate tokens issued by the authorization server and enforce permissions.
Flows you’ll see in production (and why)
Authorization Code + PKCE (recommended)
This is the standard for SPAs and mobile apps today. The client redirects the user to the IdP login, then exchanges a short-lived code for tokens. PKCE protects the code exchange so it can’t be stolen.
Client Credentials (machine-to-machine)
Used when there’s no user—service A calling service B. Tokens represent the service identity and permissions.
(You can learn more flows later, but these two cover most real systems.)
Access token vs ID token (people mix them up)
Access token:
Sent to APIs
Represents permissions (scopes/roles)
Short-lived
ID token (OIDC):
Sent to the client
Represents identity (user profile claims like subject, email, name)
Not meant for authorizing API requests
Rule: APIs should validate access tokens, not ID tokens.
JWT vs Reference (opaque) access tokens
This decision affects security, scaling, and how fast you can respond to incidents.
JWT access tokens (self-contained)
A JWT is signed and can be validated locally by your API. It can include claims (user id, roles, scopes, expiry).
Pros:
Fast validation (no database/network call)
Works well for high throughput
Good for distributed systems
Cons:
Harder to revoke immediately (until expiry)
If you put too much data in it, it becomes a “leaky database”
If compromised, it’s valid until it expires
Best for:
Short-lived access tokens (5–15 minutes)
Systems where “revocation by expiry” is acceptable for access tokens
Service-to-service calls with strict expiry
Reference (opaque) access tokens (server-side)
An opaque token is just a random string. Your API can’t understand it without calling the authorization server (introspection) or a shared token store.
Pros:
Easy immediate revocation
Central control (disable user/session instantly)
Less data exposure (token reveals nothing)
Cons:
Adds a lookup (latency + dependency)
Needs caching and high availability
Can become a bottleneck if designed poorly
Best for:
High security environments
Long-lived sessions where instant revocation matters
When you want central session management
A practical production pattern that works
Many strong systems combine both:
JWT access tokens: very short-lived (5–10 minutes)
Refresh tokens: longer-lived (days/weeks) and revocable
Optional: a revocation list or token rotation policies for higher security
Refresh tokens (why they exist)
Access tokens should be short-lived. Refresh tokens exist so users don’t have to log in every 10 minutes.
How it works:
User logs in via Authorization Code + PKCE
Client gets an access token (short) + refresh token (long)
When access token expires, client uses refresh token to get a new access token
Refresh token can be revoked (logout, suspicious activity, password change)
Important security rule:
Refresh tokens should be protected more than access tokens.
Store them securely (mobile secure storage; web: HttpOnly cookies if possible)
Use rotation (every refresh returns a new refresh token and invalidates the old one)
Detect reuse (if an old refresh token is used again, assume theft)
What “production auth” really means (the checklist)
If you want to think like a production backend engineer, validate these:
Token lifetimes: access short, refresh controlled
Revocation plan: how do you log out everywhere instantly?
RBAC/Scopes: how do you represent permissions (and keep them stable)?
Key rotation: can you rotate signing keys without downtime?
Audience/issuer checks: does your API reject tokens not meant for it?
Observability: do you log auth failures with trace context (without leaking secrets)?
Rate limiting: do you protect login/refresh endpoints?
Common mistakes (and quick fixes)
Mistake: Using ID tokens to call APIs
Fix: Use access tokens for APIs; ID tokens only for client identity.
Mistake: Long-lived JWT access tokens
Fix: Make access tokens short-lived; rely on refresh tokens.
Mistake: Putting sensitive data in JWT claims
Fix: Keep claims minimal; fetch sensitive info server-side when needed.
Mistake: No plan for revocation
Fix: Use short expiries + refresh token rotation and revocation.
What’s next in the series
Part 3 is GraphQL in production: schema design, preventing N+1, caching, and error handling—so your API stays fast and predictable under load.
Production APIs Playbook — Part 2 of 5
If you’ve ever shipped “JWT auth” and still felt unsure, you’re not alone. Most developers learn tokens as a coding trick. Production teams treat auth as a system: identity, sessions, permissions, and safe rotation.
This post explains OAuth2 and OpenID Connect (OIDC) in a way you can actually use, then clarifies the biggest confusion: when to use JWT access tokens vs opaque (reference) tokens, and how refresh tokens fit in.
What OAuth2 and OIDC actually do
OAuth2 is an authorization framework. It answers: “How can an app get permission to call an API on behalf of a user?”
OIDC is an identity layer on top of OAuth2. It answers: “Who is the user?” and provides standardized login (authentication).
A simple mental model:
OAuth2 = permission to access resources (scopes)
OIDC = login + identity claims (who the user is)
In real products, you almost always use them together.
The 4 pieces you should know (no fluff)
Resource Owner: the user
Client: your app (web/mobile/backend)
Authorization Server: the identity provider (IdP) that issues tokens
Resource Server: your API that accepts tokens
Your API should not “invent identity.” It should validate tokens issued by the authorization server and enforce permissions.
Flows you’ll see in production (and why)
Authorization Code + PKCE (recommended)
This is the standard for SPAs and mobile apps today. The client redirects the user to the IdP login, then exchanges a short-lived code for tokens. PKCE protects the code exchange so it can’t be stolen.
Client Credentials (machine-to-machine)
Used when there’s no user—service A calling service B. Tokens represent the service identity and permissions.
(You can learn more flows later, but these two cover most real systems.)
Access token vs ID token (people mix them up)
Access token:
Sent to APIs
Represents permissions (scopes/roles)
Short-lived
ID token (OIDC):
Sent to the client
Represents identity (user profile claims like subject, email, name)
Not meant for authorizing API requests
Rule: APIs should validate access tokens, not ID tokens.
JWT vs Reference (opaque) access tokens
This decision affects security, scaling, and how fast you can respond to incidents.
JWT access tokens (self-contained)
A JWT is signed and can be validated locally by your API. It can include claims (user id, roles, scopes, expiry).
Pros:
Fast validation (no database/network call)
Works well for high throughput
Good for distributed systems
Cons:
Harder to revoke immediately (until expiry)
If you put too much data in it, it becomes a “leaky database”
If compromised, it’s valid until it expires
Best for:
Short-lived access tokens (5–15 minutes)
Systems where “revocation by expiry” is acceptable for access tokens
Service-to-service calls with strict expiry
Reference (opaque) access tokens (server-side)
An opaque token is just a random string. Your API can’t understand it without calling the authorization server (introspection) or a shared token store.
Pros:
Easy immediate revocation
Central control (disable user/session instantly)
Less data exposure (token reveals nothing)
Cons:
Adds a lookup (latency + dependency)
Needs caching and high availability
Can become a bottleneck if designed poorly
Best for:
High security environments
Long-lived sessions where instant revocation matters
When you want central session management
A practical production pattern that works
Many strong systems combine both:
JWT access tokens: very short-lived (5–10 minutes)
Refresh tokens: longer-lived (days/weeks) and revocable
Optional: a revocation list or token rotation policies for higher security
Refresh tokens (why they exist)
Access tokens should be short-lived. Refresh tokens exist so users don’t have to log in every 10 minutes.
How it works:
User logs in via Authorization Code + PKCE
Client gets an access token (short) + refresh token (long)
When access token expires, client uses refresh token to get a new access token
Refresh token can be revoked (logout, suspicious activity, password change)
Important security rule:
Refresh tokens should be protected more than access tokens.
Store them securely (mobile secure storage; web: HttpOnly cookies if possible)
Use rotation (every refresh returns a new refresh token and invalidates the old one)
Detect reuse (if an old refresh token is used again, assume theft)
What “production auth” really means (the checklist)
If you want to think like a production backend engineer, validate these:
Token lifetimes: access short, refresh controlled
Revocation plan: how do you log out everywhere instantly?
RBAC/Scopes: how do you represent permissions (and keep them stable)?
Key rotation: can you rotate signing keys without downtime?
Audience/issuer checks: does your API reject tokens not meant for it?
Observability: do you log auth failures with trace context (without leaking secrets)?
Rate limiting: do you protect login/refresh endpoints?
Common mistakes (and quick fixes)
Mistake: Using ID tokens to call APIs
Fix: Use access tokens for APIs; ID tokens only for client identity.
Mistake: Long-lived JWT access tokens
Fix: Make access tokens short-lived; rely on refresh tokens.
Mistake: Putting sensitive data in JWT claims
Fix: Keep claims minimal; fetch sensitive info server-side when needed.
Mistake: No plan for revocation
Fix: Use short expiries + refresh token rotation and revocation.
What’s next in the series
Part 3 is GraphQL in production: schema design, preventing N+1, caching, and error handling—so your API stays fast and predictable under load.
The truly Limitless design subscription.
Say goodbye to expensive freelancers, and hello to limitless, lightning fast design.
