OAuth2 and OIDC
OAuth 2.0 and OpenID Connect
Understand how OAuth 2.0 delegates authorization between applications, how OpenID Connect adds verified user identity on top of it, and how to choose the right flow for each client type.
Prerequisites
Recommended Knowledge
- HTTP methods, status codes, headers, and redirects
- TLS and why transport security matters
- Cookies, sessions, and browser storage
- REST APIs and request authentication
- Authentication versus authorization
- JSON structure and Base64 URL encoding
- Public-key cryptography and digital signatures
- Trust boundaries from threat modeling
The Problem Being Solved
Suppose a reporting application needs to read a user's files from a storage platform. The naive approach is to ask the user for their storage password and log in on their behalf.
OAuth 2.0 is the framework that makes the second approach standard. It allows one application to act on a resource with limited permission, without ever seeing the user's credentials.
Simple Analogy
A hotel issues a key card that opens one room for a limited period, rather than handing over the master key. The card can be cancelled without changing every lock in the building.
OAuth 2.0 Roles
| Role | Responsibility | Example |
|---|---|---|
| Resource owner | Owns the data and grants permission | The end user |
| Client | Requests access on the owner's behalf | The reporting application |
| Authorization server | Authenticates the owner and issues tokens | The identity provider |
| Resource server | Hosts protected data and validates tokens | The storage or business API |
OAuth 2.0 Is Not Authentication
This is the most consequential misunderstanding in the topic. OAuth 2.0 is an authorization framework. An access token states that a client may perform certain operations. It does not, by itself, reliably tell the client who the user is.
Unsafe Assumption
- Treating an access token as proof of identity
- Calling a profile endpoint and trusting the result blindly
- Assuming the token was issued for this specific client
- Logging users in based on an opaque access token
Correct Approach
- Use OpenID Connect for authentication
- Validate an ID token's signature and claims
- Verify the audience matches this client
- Keep identity and API authorization distinct
What OpenID Connect Adds
OpenID Connect, commonly written as OIDC, is an identity layer built on top of OAuth 2.0. It reuses the same flows and endpoints but adds a standardized way to prove and describe user identity.
The ID Token
A signed JSON Web Token containing claims about the authentication event and the user. Unlike an access token, it is intended for the client to read and validate.
Standard Claims
A defined vocabulary for identity attributes such as subject, issuer, audience, name, and email, so clients are not dependent on provider-specific formats.
The UserInfo Endpoint
A protected endpoint returning additional profile claims, accessed using the issued access token.
Discovery and Key Publication
A well-known configuration document and a published key set allowing clients to locate endpoints and obtain signing keys programmatically.
| Aspect | OAuth 2.0 | OpenID Connect |
|---|---|---|
| Primary purpose | Delegated authorization | Authentication and identity |
| Core question | What may this client access? | Who is this user? |
| Main token | Access token | ID token |
| Token audience | The resource server | The client application |
| Token format | Opaque or JWT, provider dependent | JWT with defined claims |
| Triggering scope | API-specific scopes | The openid scope |
The Three Token Types
| Token | Purpose | Intended Consumer | Typical Lifetime |
|---|---|---|---|
| Access token | Authorizes API calls | Resource server | Short |
| ID token | Proves an authentication occurred | Client application | Short |
| Refresh token | Obtains new access tokens | Authorization server | Longer |
Example ID Token Claims
{
"iss": "https://identity.example.com",
"sub": "user-4821",
"aud": "reporting-web-client",
"exp": 1790000000,
"iat": 1789996400,
"auth_time": 1789996390,
"nonce": "n-3f9a2c7b",
"email": "user@example.com",
"email_verified": true,
"name": "Example User"
}
| Claim | Meaning | Why It Must Be Checked |
|---|---|---|
iss |
Issuing authorization server | Rejects tokens from an unexpected issuer |
sub |
Stable user identifier | Safer account key than a changeable email |
aud |
Intended client | Prevents accepting a token issued for another app |
exp |
Expiry time | Prevents indefinite token reuse |
iat |
Issue time | Supports freshness evaluation |
nonce |
Value bound to the original request | Detects token replay into a different session |
The Authorization Code Flow
The authorization code flow is the recommended default for applications acting on behalf of a user. The browser never carries tokens in its address bar, because a short-lived code is exchanged for tokens over a direct back-channel request.
- The client redirects the user to the authorization server.
- The user authenticates and approves the requested scopes.
- The server redirects back with a single-use authorization code.
- The client verifies the returned state value.
- The client exchanges the code at the token endpoint.
- The server returns access, ID, and possibly refresh tokens.
- The client validates the ID token before establishing a session.
- The client calls APIs using the access token.
Authorization Request
GET /authorize
?response_type=code
&client_id=reporting-web-client
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20profile%20email%20reports.read
&state=stt-7c1d9e4a
&nonce=n-3f9a2c7b
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: identity.example.com
Token Exchange
{
"grant_type": "authorization_code",
"code": "auth-code-9f2b71c4",
"redirect_uri": "https://app.example.com/callback",
"client_id": "reporting-web-client",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}
Token Response
{
"access_token": "opaque-or-jwt-access-token",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "refresh-token-value",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile email reports.read"
}
PKCE: Proof Key for Code Exchange
PKCE protects the authorization code against interception. The client generates a random secret before starting the flow, sends only its hash in the authorization request, and reveals the original value during the token exchange.
An attacker who steals the authorization code cannot exchange it, because they do not possess the verifier that matches the challenge recorded at the authorization server.
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
async function createPkcePair() {
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
const codeVerifier = base64UrlEncode(randomBytes.buffer);
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(codeVerifier)
);
return {
codeVerifier: codeVerifier,
codeChallenge: base64UrlEncode(digest),
codeChallengeMethod: "S256"
};
}
State and Nonce
These two parameters defend against different attacks and are frequently confused.
State
- Random value tied to the browser session
- Returned unchanged on the redirect
- Defends against cross-site request forgery
- Verified before the code is exchanged
Nonce
- Random value sent in the authentication request
- Embedded as a claim inside the ID token
- Defends against ID token replay
- Verified during ID token validation
Choosing the Right Grant
| Grant | Use Case | Status |
|---|---|---|
| Authorization code with PKCE | Web apps, single-page apps, mobile apps | Recommended default |
| Client credentials | Service-to-service calls with no user present | Recommended for machine identity |
| Device authorization | Televisions, consoles, and input-limited devices | Recommended for that context |
| Refresh token | Renewing access without re-prompting the user | Recommended with rotation |
| Implicit | Historically used by browser applications | Discouraged in current guidance |
| Resource owner password | Client collects the user's password directly | Discouraged in current guidance |
Client Credentials for Service-to-Service
When no user is involved, the client authenticates as itself. A background indexing job calling an internal API is a typical example.
{
"grant_type": "client_credentials",
"client_id": "search-indexer",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": "eyJhbGciOiJSUzI1NiJ9...",
"scope": "catalog.read index.write"
}
Machine Identity Practices
- Prefer signed client assertions over shared secrets
- Issue a distinct identity per workload
- Grant only the scopes that workload requires
- Rotate credentials and keys on a defined schedule
- Store secrets in a managed secret store, never in source
- Log and alert on unexpected scope usage
Validating Tokens Correctly
Validation is where most implementation defects appear. Every check below exists because skipping it enables a specific attack.
Verify the Signature
Retrieve the issuer's published keys, select the key matching the token's key identifier, and verify the signature. Cache keys, and refresh on rotation.
Enforce the Expected Algorithm
Accept only algorithms your configuration permits. Never let the token's own header dictate the verification method, and never accept an unsigned token.
Check Issuer and Audience
Confirm the issuer matches the configured authorization server and the audience matches this application or API.
Check Time Claims
Reject expired tokens and tokens not yet valid, allowing only a small tolerance for clock differences.
Check Nonce and Scopes
Confirm the ID token nonce matches the value stored for this login attempt, and confirm the access token carries the scopes the endpoint requires.
async function validateIdToken(idToken, expected) {
const { header, payload, signature } = decodeJwt(idToken);
if (!expected.allowedAlgorithms.includes(header.alg)) {
throw new Error("Unsupported signing algorithm");
}
const signingKey = await getSigningKey(
expected.issuer,
header.kid
);
const signatureValid = await verifySignature(
idToken,
signingKey,
header.alg
);
if (!signatureValid) {
throw new Error("Invalid token signature");
}
if (payload.iss !== expected.issuer) {
throw new Error("Unexpected issuer");
}
const audiences = Array.isArray(payload.aud)
? payload.aud
: [payload.aud];
if (!audiences.includes(expected.clientId)) {
throw new Error("Token audience mismatch");
}
const nowSeconds = Math.floor(Date.now() / 1000);
if (payload.exp <= nowSeconds - expected.clockSkewSeconds) {
throw new Error("Token expired");
}
if (payload.nonce !== expected.nonce) {
throw new Error("Nonce mismatch");
}
return payload;
}
Opaque Tokens and Introspection
Not all access tokens are JWTs. An opaque token is a reference that carries no readable content, and the resource server asks the authorization server about its validity.
| Property | JWT Access Token | Opaque Access Token |
|---|---|---|
| Validation | Local signature verification | Introspection call to the issuer |
| Latency | Minimal per request | Network call unless cached |
| Revocation | Effective only at expiry unless checked | Effective immediately |
| Content exposure | Claims readable by anyone holding it | No embedded information |
| Availability coupling | Independent of the issuer at request time | Depends on issuer availability |
Refresh Tokens and Rotation
Refresh tokens let a client obtain new access tokens without interrupting the user. Because they are long-lived, they are a high-value target and require stronger handling.
CREATE TABLE refresh_tokens (
token_id VARCHAR(64) PRIMARY KEY,
family_id VARCHAR(64) NOT NULL,
subject_id VARCHAR(64) NOT NULL,
client_id VARCHAR(64) NOT NULL,
token_hash VARCHAR(128) NOT NULL,
issued_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
consumed_at TIMESTAMP NULL,
revoked_at TIMESTAMP NULL
);
UPDATE refresh_tokens
SET revoked_at = CURRENT_TIMESTAMP
WHERE family_id = :family_id
AND revoked_at IS NULL;
If a previously consumed refresh token is presented again, the authorization server should assume theft and revoke the entire token family, forcing a fresh authentication.
Scopes Versus Permissions
A scope describes what the client was authorized to request. It does not describe what the user is entitled to access. Both checks are required.
reports.read, so the API returns
the requested report without verifying that this particular user
may view that particular report.
async function getReport(request, response) {
const claims = request.tokenClaims;
if (!claims.scope.split(" ").includes("reports.read")) {
return response.status(403).json({
error: "insufficient_scope"
});
}
const report = await reportRepository.findById(
request.params.reportId
);
if (!report) {
return response.status(404).json({ error: "not_found" });
}
const permitted = await authorizationService.canView(
claims.sub,
report
);
if (!permitted) {
return response.status(404).json({ error: "not_found" });
}
return response.json(report);
}
Threats and Mitigations
| Threat | Description | Mitigation |
|---|---|---|
| Open redirect | Redirect URI is manipulated to an attacker site | Exact-match registered redirect URIs |
| Code interception | Authorization code is captured in transit | PKCE with the S256 method |
| CSRF on callback | Attacker initiates a flow in the victim's browser | State parameter bound to the session |
| ID token replay | A captured token is reused in another session | Nonce validation and short lifetimes |
| Token substitution | A token issued for another client is presented | Strict audience validation |
| Algorithm confusion | Token header dictates weaker verification | Allow-list algorithms server-side |
| Token theft via scripting | Malicious script reads browser-stored tokens | HttpOnly cookies and content security policy |
| Refresh token theft | Stolen refresh token grants prolonged access | Rotation with reuse detection |
| Excessive scope | Client requests more access than needed | Granular scopes and consent review |
| Consent phishing | A malicious app requests broad permissions | App verification and admin consent policies |
Where Browser Applications Should Store Tokens
Local Storage
- Readable by any script on the page
- Exposed by a single scripting vulnerability
- Exposed by a compromised third-party library
- Persists beyond the browsing session
Backend-Managed Session
- Tokens held server-side, not in the browser
- Session cookie marked HttpOnly and Secure
- SameSite attribute limits cross-site sending
- Server can revoke the session immediately
The backend-for-frontend pattern places a server component between the browser and the APIs. The browser holds only a session cookie, while the server holds the tokens and attaches them to outbound API calls.
Logout and Session Termination
Clearing a local session is not a complete logout. Several layers may retain valid state.
Complete Logout Requires
- Clearing the application session and cookie
- Revoking the refresh token at the authorization server
- Ending the session at the identity provider
- Notifying other applications sharing the session
- Accepting that issued access tokens remain valid until expiry
- Recording the logout event in the audit trail
Discovery Configuration
{
"issuer": "https://identity.example.com",
"authorization_endpoint": "https://identity.example.com/authorize",
"token_endpoint": "https://identity.example.com/token",
"userinfo_endpoint": "https://identity.example.com/userinfo",
"jwks_uri": "https://identity.example.com/.well-known/jwks.json",
"end_session_endpoint": "https://identity.example.com/logout",
"response_types_supported": ["code"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"code_challenge_methods_supported": ["S256"]
}
Using discovery allows clients to adapt to endpoint and key changes without redeployment, provided the document is retrieved over a verified connection and cached sensibly.
Monitoring and Detection
Signals Worth Tracking
- Failed token validations by reason
- Audience and issuer mismatch attempts
- Refresh token reuse detections
- Authorization failures by client and endpoint
- Unusual scope combinations being requested
- Token requests from unexpected regions
- Spikes in consent grants for a new client
- Key rotation and signing key fetch failures
- Latency and error rate of the introspection endpoint
Common Implementation Mistakes
Weak Implementation
- Using OAuth alone as a login mechanism
- Reading claims without verifying signatures
- Skipping audience validation
- Omitting state or nonce
- Allowing wildcard redirect URIs
- Storing tokens in browser local storage
- Issuing long-lived access tokens
- Treating scope as complete authorization
- Linking accounts by unverified email
Strong Implementation
- Uses OIDC for identity and OAuth for API access
- Fully validates signature and claims
- Enforces exact redirect URI matching
- Applies PKCE on every code flow
- Keeps tokens out of the browser where possible
- Issues short access tokens with rotating refresh
- Checks scope and resource permission separately
- Keys accounts on issuer and subject
- Monitors and alerts on validation failures
System Design Interview Discussion
| Question | What Your Answer Should Cover |
|---|---|
| Which flow and why? | Client type, credential confidentiality, and PKCE |
| How is the user identified? | ID token validation and the subject claim |
| Where do tokens live? | Server-side session versus browser storage trade-offs |
| How are tokens validated? | Signature, issuer, audience, expiry, nonce, and scope |
| How is access revoked? | Refresh revocation, short lifetimes, and introspection |
| How do services authenticate? | Client credentials with per-workload identity |
| What if the identity provider is unavailable? | Key caching, graceful degradation, and retry behavior |
| How is misuse detected? | Reuse detection, anomaly monitoring, and audit logs |
Implementation Checklist
Production Checklist
- Use the authorization code flow with PKCE
- Register exact redirect URIs without wildcards
- Send and verify state on every request
- Send and verify nonce for authentication flows
- Verify token signatures against published keys
- Allow-list acceptable signing algorithms
- Validate issuer, audience, and expiry
- Key user accounts on issuer and subject
- Request the minimum necessary scopes
- Enforce resource-level authorization independently
- Keep access token lifetimes short
- Rotate refresh tokens and detect reuse
- Store tokens server-side where feasible
- Support revocation and end-session endpoints
- Cache signing keys and handle rotation gracefully
- Enforce TLS on every endpoint in the flow
- Audit consent grants and authorization failures
- Test expired, tampered, and mismatched-audience tokens
Knowledge Check
What problem does OAuth 2.0 solve?
It allows an application to access resources on a user's behalf with limited, revocable permission, without receiving the user's credentials.
How does OIDC differ from OAuth 2.0?
OIDC adds an authentication layer, introducing a signed ID token with standard identity claims intended for the client.
What does PKCE protect against?
Interception of the authorization code, by requiring a secret verifier that only the legitimate client possesses.
Why validate the audience claim?
Without it, a token legitimately issued for a different application could be accepted, allowing token substitution.
Why is scope insufficient for authorization?
Scope describes what the client may attempt. The API must still verify that the specific user is entitled to the specific resource.
Summary
OAuth 2.0 is a delegated authorization framework built around four roles and several grant types. It replaces credential sharing with limited, expiring, revocable tokens issued by an authorization server.
OpenID Connect layers authentication on top of OAuth 2.0, introducing the ID token with standardized claims, a UserInfo endpoint, and discovery metadata. Access tokens authorize API calls, ID tokens establish identity, and refresh tokens renew access.
The authorization code flow with PKCE is the recommended default for user-facing clients, while client credentials suits service-to-service communication. Implicit and password grants are discouraged in current guidance.
Correctness depends on disciplined validation: verify the signature, enforce the expected algorithm, check issuer, audience, expiry, and nonce, and then enforce resource-level authorization separately from scope.
Key Takeaway
OAuth 2.0 grants access; OpenID Connect proves identity. Use the authorization code flow with PKCE, validate every token claim rather than trusting its contents, keep access tokens short-lived with rotating refresh tokens, and treat scope as a ceiling rather than as a complete authorization decision.