Skip to content

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
The problem OAuth solves: third-party apps shouldn't see user passwords
Source: Wikimedia Commons. CC BY-SA / public domain.

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:

  • openid scope: 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 localStorage instead of httpOnly cookies for SPAs.
  • Skipping state parameter, exposing the flow to CSRF.
  • Forgetting nonce on OIDC, exposing replay.
  • Accepting id_token as 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 string
  • code_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 /token to renew access tokens
  • Refresh-token rotation + reuse detection mandatory

OIDC additions#

  • openid scope triggers ID token
  • ID token = JWT with iss, sub, aud, exp, iat, nonce
  • /.well-known/openid-configuration discovery
  • /userinfo returns profile claims
  • JWKS endpoint serves signing keys

JWT vs opaque#

  • JWT: local validation, hard revoke
  • Opaque: /introspect call, 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 redirect
  • nonce: replay defense on OIDC ID token
  • aud: validate audience on every token check
  • iss: validate issuer on every token check

Common pitfalls#

  • Refresh tokens in localStorage
  • Skipping state or nonce
  • 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.

  • 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

Video walkthrough

OAuth 2 Explained In Simple Terms : via ByteByteGo