OAuth2 and OIDC Flows
OAuth2 and OIDC Flows
OAuth2 is an authorization delegation framework that issues access tokens scoped to a specific resource; OIDC (OpenID Connect) is an identity layer on top of OAuth2 that adds an ID token carrying verified user claims — the two are distinct protocols that are almost always deployed together.
Core Idea
OAuth2 and OIDC together address three distinct concerns:
- Authorization delegation — an application acts on a user's behalf without receiving the user's password; instead, the user grants permission at the Identity Provider (IdP) and the IdP issues a scoped access token
- Token scoping — access tokens are narrowly scoped to specific resources and expire quickly, limiting blast radius if a token is compromised
- Identity assertion (OIDC only) — the ID token is a signed JWT proving who the user is; the access token proves what the user is allowed to do — these are separate concerns carried by separate tokens
OAuth2 defines grant types (flows) for different client scenarios: the grant type determines how the client obtains tokens, whether a human user is involved, and what security properties the flow provides. Choosing the wrong grant for a client type is a common security mistake — for example, using Client Credentials in a flow that involves a human user, or using Implicit Flow (now deprecated) where Authorization Code + PKCE is required.
When NOT to Use
- Do not use Authorization Code Flow without PKCE — vulnerable to authorization code interception attacks; PKCE is mandatory per RFC 9700 (OAuth 2.0 Security BCP) and OAuth 2.1 for all clients
- Do not use Implicit Flow — deprecated in OAuth 2.1; returns tokens directly in the URL fragment, bypassing code exchange entirely; use Authorization Code + PKCE instead
- Do not use Resource Owner Password Credentials (ROPC) — requires the application to receive the user's password directly; violates zero-trust and separation of concerns; deprecated in OAuth 2.1; IdPs are removing support
- Do not use Client Credentials for flows involving a human user — Client Credentials is for machine-to-machine only; it issues no user context (
subclaim is absent or refers to the client, not a person) - Do not implement custom token formats instead of JWT — opaque tokens require token introspection on every request; JWT allows local signature validation at scale using the IdP's JWKS endpoint
How It Works
Grant Type Taxonomy
| Grant Type | Client Type | Has User Context | Token Response |
|---|---|---|---|
| Authorization Code + PKCE | Server-side confidential client or SPA | Yes — user authenticates at IdP | access_token + refresh_token + id_token (if OIDC) |
| Client Credentials | Server-side machine (no user) | No — client authenticates as itself | access_token only |
| Device Authorization Flow | CLI, IoT, smart TV (no browser) | Yes — user authenticates on secondary device | access_token + refresh_token |
| (deprecated) Implicit | SPA public client (legacy) | Yes | access_token in URL fragment — AVOID |
Authorization Code + PKCE
The client redirects the user to the IdP, where the user authenticates. The IdP issues a short-lived authorization code and redirects back to the client. The client then exchanges the code — together with a PKCE code_verifier and, for confidential clients, a client_secret — for tokens at the token endpoint. PKCE binds the code to the initiating client: an attacker who intercepts the authorization code cannot exchange it without possessing the original code_verifier.
For the BFF-specific implementation of this flow (confidential client, Spring Security, Redis session), see OAuth2-BFF-Pattern and BFF-For-SPA. For the PKCE cryptographic mechanism, see PKCE.
Client Credentials
No user redirect occurs. The client sends its client_id and client_secret directly to the token endpoint; the IdP authenticates the client as itself and returns an access_token with no sub (subject) claim representing a human user. This grant is appropriate for service-to-service calls, background jobs, scheduled tasks, and CLI tools acting as the service itself — not on behalf of a user. The access token should be short-lived (≤1 hour) and cached until near expiry to avoid overloading the token endpoint on every outbound call.
Device Authorization Flow (RFC 8628)
The client (a CLI, IoT device, or smart TV) requests a device_code and a human-readable user_code from the IdP's device authorization endpoint. The client displays the user_code to the user along with a verification_uri. The user visits the URI on any browser-capable device — typically a phone or laptop — and enters the code to authenticate. Meanwhile, the client polls the token endpoint at the prescribed interval until the user completes authentication, at which point the IdP returns access_token and refresh_token. This flow requires no browser on the device running the client, making it the correct grant for headless environments.
OAuth2 vs OIDC
| Dimension | OAuth2 | OIDC |
|---|---|---|
| Purpose | Authorization delegation | Identity assertion |
| Token type | access_token (opaque or JWT) | access_token + id_token (JWT) |
| User claims | Not defined | sub, email, name, etc. in id_token |
| Scope | Custom resource scopes | openid (required) + profile, email, etc. |
| Spec | RFC 6749, RFC 9700 | OpenID Connect Core 1.0 |
OIDC is not a replacement for OAuth2 — it adds identity on top of the OAuth2 authorization framework. The openid scope in the authorization request triggers OIDC: the IdP includes an id_token in the response alongside the access_token, and the id_token carries signed claims about the authenticated user.
Mermaid Sequence Diagram: Authorization Code + PKCE Flow
sequenceDiagram
participant Client
participant Browser
participant IdP
Client->>Browser: Redirect to /authorize?code_challenge=H(verifier)
Browser->>IdP: GET /authorize (user sees login page)
IdP-->>Browser: 302 redirect_uri?code=AUTH_CODE
Browser->>Client: GET /callback?code=AUTH_CODE
Client->>IdP: POST /token (code + code_verifier + client_secret)
IdP-->>Client: {access_token, refresh_token, id_token}
Client-->>Browser: Set-Cookie (session) or return token
TypeScript Example
// jose 6.2.2 — for validating the access_token received from the token endpoint
/**
* Obtains an access token using the Client Credentials grant.
* Use for service-to-service calls where no human user is involved.
*/
async function fetchClientCredentialsToken(
clientId: string,
clientSecret: string,
tokenEndpoint: string
): Promise<string> {
const body = new URLSearchParams({
grant_type: "client_credentials",
client_id: clientId,
client_secret: clientSecret,
});
const response = await fetch(tokenEndpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json() as { access_token: string; expires_in: number };
// Cache this token until expiresAt - 60s; do not fetch a new token on every request
return data.access_token;
}Java Example
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.stereotype.Service;
/**
* Obtains service-to-service access tokens using Client Credentials grant.
* Spring Security caches and refreshes the token automatically; no manual caching needed.
*/
@Service
public class TokenService {
private final OAuth2AuthorizedClientManager authorizedClientManager;
private final ClientRegistrationRepository clientRegistrationRepository;
public TokenService(
OAuth2AuthorizedClientManager authorizedClientManager,
ClientRegistrationRepository clientRegistrationRepository) {
this.authorizedClientManager = authorizedClientManager;
this.clientRegistrationRepository = clientRegistrationRepository;
}
public String getServiceToken() {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("service-client")
.principal("service-account")
.build();
OAuth2AuthorizedClient authorizedClient =
authorizedClientManager.authorize(authorizeRequest);
if (authorizedClient == null) {
throw new IllegalStateException("Client credentials grant failed");
}
return authorizedClient.getAccessToken().getTokenValue();
}
}For Authorization Code + PKCE with BFF, Spring Security handles the full flow automatically — see OAuth2-BFF-Pattern.
Related Concepts
| Concept | Relationship |
|---|---|
| JWT | The token format most commonly used for access_token and id_token in OAuth2/OIDC |
| OAuth2-BFF-Pattern | BFF-specific implementation of Authorization Code + PKCE for browser SPAs |
| BFF-For-SPA | End-to-end SPA authentication flow built on Authorization Code + PKCE |
| PKCE | PKCE cryptographic mechanism used in Authorization Code Flow |
| Session-Management | Server-side session vs stateless JWT token trade-offs after authentication |
| Token-Relay-Pattern | How authenticated access tokens are forwarded to downstream services |