OAuth 2.0 and OIDC internals
How OAuth 2.0 authorization code flow + PKCE works step by step, what OIDC adds on top, token introspection vs. local JWT verification, the JWKS endpoint, and common implementation mistakes.
The problem
Your team ships a new SPA. Users sign up with a username and password that you store in your own database. A third-party analytics service needs to pull user profile data from your API. The simplest integration? The user gives the analytics service their password. Now that service can call your API as the user.
Three months later, you discover the analytics vendor stored passwords in plaintext and got breached. Every user who shared their credentials is compromised across your system and every other service where they reused that password. You have no way to revoke the analytics service's access without resetting the user's password entirely.
The core issue: there is no separation between "proving who you are" (authentication) and "granting limited access to your stuff" (authorization). The user's password does both, and sharing it means sharing everything. This is the problem OAuth 2.0 solves, and OpenID Connect extends.
What it is
OAuth 2.0 is an authorization framework that lets users grant third-party applications scoped access to their resources without sharing credentials. Instead of handing over a password, the user approves a specific set of permissions, and the authorization server issues a token representing that approval.
Think of it like a hotel key card. The front desk (authorization server) verifies your identity once and gives you a card (token) that opens your room and the gym but not the staff office. You never give the hotel's master key to the room service staff.
OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. OAuth answers "is this app allowed to access this resource?" OIDC answers "who is the user?" It adds a standardized ID token (a signed JWT) containing identity claims like email, name, and a stable user identifier.
The access_token is for authorization (calling APIs on behalf of the user). The id_token added by OIDC is for authentication (verifying who the user is). I find that candidates who can articulate this distinction clearly stand out immediately in interviews.
How it works
The authorization code flow with PKCE is the recommended OAuth 2.0 flow for both server-side apps and single-page applications. The older "implicit flow" (returning tokens directly in URL fragments) is deprecated because tokens end up in browser history and server logs.
The flow has three critical security properties. First, the user's credentials never touch the client application. Second, the authorization code is useless without the code_verifier (PKCE). Third, the state parameter prevents CSRF attacks where an attacker tricks the client into using the attacker's authorization code.
Authorization code flow with PKCE
This is the recommended flow for server-side apps and SPAs. The old "implicit flow" (directly returning tokens in URL fragments) is deprecated because tokens leaked via browser history, Referer headers, and server logs.
Step 1: Generate PKCE challenge
The client generates a code_verifier (a cryptographically random 43-128 character string) and computes its SHA-256 hash as the code_challenge. This verifier stays on the client and is never sent to the authorization server until the token exchange.
import secrets, hashlib, base64
code_verifier = secrets.token_urlsafe(64) # 86-char random string
challenge_bytes = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(challenge_bytes).rstrip(b'=').decode()
Step 2: Redirect to authorization server
The client redirects the user's browser to the authorization endpoint with the challenge:
GET /authorize?
response_type=code
&client_id=client_123
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=random_csrf_token
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256
Step 3: User authenticates and consents
The authorization server shows a login page. The user authenticates and grants consent for the requested scopes. The server redirects back with an authorization code:
https://app.example.com/callback?code=AUTH_CODE&state=random_csrf_token
Step 4: Exchange code for tokens (back-channel)
The client sends the authorization code plus the original code_verifier to the token endpoint. This is a server-to-server call, not a browser redirect:
POST /token
grant_type=authorization_code
code=AUTH_CODE
redirect_uri=https://app.example.com/callback
client_id=client_123
code_verifier=ORIGINAL_VERIFIER
The authorization server computes SHA256(code_verifier) and compares it to the code_challenge from Step 2. If they match, tokens are issued.
Why PKCE is non-negotiable
Without PKCE, an attacker who intercepts the authorization code (via a malicious browser extension, a compromised redirect, or a mobile app deep link hijack) can exchange it for tokens. PKCE binds the code to the client that initiated the flow. Even OAuth confidential clients should use PKCE as defense-in-depth.
State parameter: The client must verify that state in the callback matches what it originally sent. This prevents CSRF attacks where an attacker initiates the flow from their own account and tricks the victim into completing it, linking the attacker's identity to the victim's session.
Token lifecycle: access, refresh, and ID tokens
The token endpoint returns up to three tokens, each with a distinct purpose and lifetime.
Access tokens are short-lived (typically 5 to 60 minutes). They are sent with every API request as a Bearer token. Short lifetimes limit the blast radius if a token is leaked.
Refresh tokens are long-lived (days, weeks, or until explicitly revoked). When the access token expires, the client uses the refresh token to get a new access token without requiring the user to re-authenticate. My recommendation: always use refresh token rotation, where the authorization server issues a new refresh token with each use and invalidates the old one.
ID tokens are used once at login to establish the user's identity. They should not be sent to resource servers or used as API credentials. The ID token tells the client "who logged in." The access token tells the resource server "what this client is allowed to do."
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.