OAuth 2.0 and OIDC#
OAuth 2.0 OIDC flows let a third-party app act on a user's behalf without ever seeing the user's password. OAuth 2.0 grants access (delegated authorization); OpenID Connect (OIDC) adds identity by riding on the same flow and returning an extra ID token.
sequenceDiagram
participant U as User
participant App as Client app
participant AS as Authorization server
participant API as Resource server
U->>App: click "Sign in with X"
App->>AS: redirect, authorization code + PKCE
AS->>U: login + consent
U->>AS: approve
AS->>App: redirect with code
App->>AS: exchange code + verifier
AS->>App: access_token + id_token + refresh_token
App->>API: GET /me with Bearer access_token
API->>App: 200 + user data
Four roles you must name on the whiteboard: the resource owner (the human), the client (the app), the authorization server (issues tokens), and the resource server (validates tokens and serves data). Modern apps almost always use the authorization code flow with PKCE, get back an access token (often a JWT) plus an OIDC ID token, and store nothing but a refresh token for renewal.
Problem statement (interviewer prompt)
Design the authorization layer for a SaaS that lets users sign in with Google, lets third-party apps call your API on a user's behalf, and lets backend services call each other. Cover OAuth 2.0 grant types, PKCE, the OIDC layer, refresh tokens, scopes versus claims, and JWT versus opaque tokens.
OAuth 2.0 is authorization delegation (RFC 6749, with the modern guidance in OAuth 2.1). OIDC sits on top and adds federated identity by returning a signed ID token.
The four roles#
| Role | Who | What it does |
|---|---|---|
| Resource owner (RO) | the human user | grants consent |
| Client | your app, mobile, SPA, server | requests tokens |
| Authorization server (AS) | Google, Okta, your IDP | authenticates user, issues tokens |
| Resource server (RS) | the API | validates tokens, returns data |
Grant types#
flowchart TB
Q[Choose grant]
Q --> User[User signs in?]
Q --> M2M[Service to service?]
Q --> Limited[Device with no browser?]
User --> AC[Authorization code + PKCE]
M2M --> CC[Client credentials]
Limited --> DC[Device code]
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
class Q client;
class User,M2M,Limited service;
class AC,CC,DC service;
| Grant | When to use |
|---|---|
| Authorization code + PKCE | every interactive user login, web, SPA, mobile, desktop |
| Client credentials | machine-to-machine, no user, service has its own identity |
| Device code | TVs, CLIs, anything without a browser keyboard |
| Refresh token | renew an access token without prompting the user again |
| Implicit (deprecated) | old SPA flow, replaced by code + PKCE |
| Password / ROPC (deprecated) | old direct-credential flow, never use today |
Authorization code + PKCE#
sequenceDiagram
participant U as User
participant C as Client app
participant AS as Authorization server
participant API as Resource server
Note over C: generate code_verifier, code_challenge = SHA256
C->>AS: /authorize?client_id&redirect_uri&code_challenge&scope&state
AS->>U: login + consent
U->>AS: approve
AS->>C: redirect with ?code=abc&state=xyz
C->>AS: POST /token, code + code_verifier + client_id
AS->>C: access_token, id_token, refresh_token
C->>API: GET /me, Authorization Bearer access_token
API->>AS: (or local) verify token
API->>C: 200 + JSON
PKCE (Proof Key for Code Exchange, RFC 7636) was originally for mobile, now required for every public client. It defeats authorization-code interception by binding the code to a one-shot verifier the attacker cannot guess.
Client credentials#
POST /token
grant_type=client_credentials
client_id=svc-orders
client_secret=... # or private_key_jwt / mTLS
scope=billing.read inventory.write
No user, no refresh token. The service authenticates and gets an access token. For higher security, use private key JWT or mTLS client auth instead of a static secret.
Device code#
device asks AS for a code, displays code + short URL to user
user opens the URL on phone, enters code, approves
device polls /token until AS returns access_token
Used for Apple TV apps, GitHub CLI, gh auth login, kubectl OIDC, and similar.
Refresh tokens#
sequenceDiagram
participant C as Client
participant AS as Auth server
C->>AS: access_token expires in 1h
Note over C: 55 minutes later
C->>AS: POST /token, grant_type=refresh_token, refresh_token=...
AS->>C: new access_token, new refresh_token (rotation)
Note over C: store the new refresh token, discard old
- Refresh tokens are long-lived; treat them as sensitive credentials.
- Rotate on each use; if you ever see the same refresh token used twice, revoke the family (reuse detection).
- Bind refresh tokens to the client and (optionally) to a device fingerprint via DPoP or mTLS sender-constrained tokens.
OIDC: identity on top of OAuth#
OIDC (OpenID Connect 1.0) adds:
openidscope: signals "give me an ID token".- ID token: a JWT proving who the user is to the client (not to the API).
- Userinfo endpoint: returns profile claims (
email,name,picture). - Discovery document at
/.well-known/openid-configuration.
id_token (decoded):
{
"iss": "https://login.example.com",
"sub": "u_84219",
"aud": "client-app-123",
"exp": 1715000000,
"iat": 1714996400,
"nonce": "n-0S6_WzA2Mj",
"email": "ada@example.com",
"email_verified": true
}
The client validates: signature against the AS's JWKS, iss, aud, exp, nonce. Never use an ID token to authenticate to your API. The ID token is for the client; the access token is for the API.
JWT vs opaque access tokens#
| JWT access token | Opaque access token | |
|---|---|---|
| Format | self-contained, signed | random string |
| Validation | local, verify signature + claims | call AS /introspect |
| Revocation | hard (must check denylist) | easy, AS just deletes it |
| Latency | zero per-request | extra RTT per request (cache it) |
| Size | larger | small |
| Best for | high-volume APIs, microservices | secret-bearing tokens, easy revoke |
Most platforms ship JWT access tokens with 5-60 minute lifetimes plus refresh tokens, and accept the revocation lag.
Scopes vs claims#
- Scopes describe what the token may do:
orders:read,billing.write,openid profile email. - Claims describe who the subject is or attributes about them:
sub,email,groups,tenant.
A common mistake is treating scope as an identity attribute or groups as a permission. Keep authorization checks against scopes (or explicit permissions derived from claims), not raw role strings.
Token introspection and revocation#
POST /introspect # RFC 7662
token=eyJ...
-> { "active": true, "scope": "...", "exp": ... }
POST /revoke # RFC 7009
token=eyJ...
Introspection lets a resource server check opaque tokens. Revocation tells the AS to invalidate a token (especially refresh tokens after logout).
Common pitfalls#
- Storing refresh tokens in
localStorageinstead of httpOnly cookies for SPAs. - Skipping
stateparameter, exposing the flow to CSRF. - Forgetting
nonceon OIDC, exposing replay. - Accepting
id_tokenas API auth (wrong audience). - Long-lived access tokens without refresh-token rotation.
- Confused-deputy: forwarding the user's access token to a downstream service without scope narrowing. Use token exchange (RFC 8693) instead.
Where this shows up#
- OAuth SSO problem: the full system design for an IDP serving thousands of clients.
- API gateways: validate JWT access tokens, attach claims to upstream requests.
- Service mesh: mTLS for machine identity, OAuth for user delegation on top.
Quick reference#
Four roles#
- Resource owner (RO): the human user
- Client: web/SPA/mobile/server app
- Authorization server (AS): issues tokens (Google, Okta, your IDP)
- Resource server (RS): the API that validates tokens
Grant types#
- Authorization code + PKCE: default for user logins, all client types
- Client credentials: machine-to-machine, no user
- Device code: TVs, CLIs, no browser keyboard
- Refresh token: renew access token without user prompt
- Implicit: deprecated
- Password / ROPC: deprecated, never use
PKCE#
code_verifier: random 43-128 byte stringcode_challenge = SHA256(code_verifier), base64url- Sent at
/authorize, verified at/token - Required for all public clients (mobile, SPA)
Tokens#
- Access token: presented to API, scoped, short-lived (5-60 min)
- ID token (OIDC): identity proof for the client, never sent to API
- Refresh token: long-lived, used at
/tokento renew access tokens - Refresh-token rotation + reuse detection mandatory
OIDC additions#
openidscope triggers ID token- ID token = JWT with
iss,sub,aud,exp,iat,nonce /.well-known/openid-configurationdiscovery/userinforeturns profile claims- JWKS endpoint serves signing keys
JWT vs opaque#
- JWT: local validation, hard revoke
- Opaque:
/introspectcall, easy revoke - Most platforms use JWT access tokens with short TTL
Scopes vs claims#
- Scopes = permissions the token grants (
orders:read) - Claims = facts about the subject (
sub,email,tenant) - Don't conflate roles in claims with authorization decisions
Required parameters#
state: CSRF defense on the redirectnonce: replay defense on OIDC ID tokenaud: validate audience on every token checkiss: validate issuer on every token check
Common pitfalls#
- Refresh tokens in localStorage
- Skipping
stateornonce - Using ID token as API auth
- Long access-token TTL without rotation
- Forwarding user tokens downstream without scope narrowing (use RFC 8693 token exchange)
Standards#
- RFC 6749 - OAuth 2.0
- RFC 7636 - PKCE
- RFC 7009 - Token revocation
- RFC 7662 - Token introspection
- RFC 8693 - Token exchange
- RFC 9068 - JWT profile for access tokens
- OpenID Connect Core 1.0
- OAuth 2.1 - consolidated best practice
Refs#
- oauth.net BCP
- OIDC spec
- Okta and Auth0 implementation guides
FAQ#
What is the difference between OAuth 2.0 and OpenID Connect?#
OAuth 2.0 grants delegated access to APIs. OpenID Connect sits on top and adds federated identity by returning a signed ID token that proves who the user is.
Why does OAuth need PKCE?#
PKCE binds the authorization code to the client that started the flow, blocking interception attacks on mobile and single-page apps that cannot keep a client secret.
What is the difference between an access token and an ID token?#
The access token authorises API calls. The ID token, defined by OIDC, is a JWT containing identity claims like sub, email, and name, and is consumed by the client.
When should I use a refresh token?#
Use a refresh token to obtain new short-lived access tokens without prompting the user. Rotate refresh tokens and bind them to the device for safety.
Are JWTs required for OAuth 2.0?#
No. OAuth permits opaque tokens validated by introspection. Many providers use JWT access tokens for performance, but the spec leaves the format open.
Related Topics#
- JWT Internals: the format used by ID tokens and most access tokens
- Web Security: CSRF, XSS, and where SameSite cookies meet OAuth
- OAuth SSO: the system design problem that puts these pieces together