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:

  1. 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
  2. Token scoping — access tokens are narrowly scoped to specific resources and expire quickly, limiting blast radius if a token is compromised
  3. 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 (sub claim 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 TypeClient TypeHas User ContextToken Response
Authorization Code + PKCEServer-side confidential client or SPAYes — user authenticates at IdPaccess_token + refresh_token + id_token (if OIDC)
Client CredentialsServer-side machine (no user)No — client authenticates as itselfaccess_token only
Device Authorization FlowCLI, IoT, smart TV (no browser)Yes — user authenticates on secondary deviceaccess_token + refresh_token
(deprecated) ImplicitSPA public client (legacy)Yesaccess_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

DimensionOAuth2OIDC
PurposeAuthorization delegationIdentity assertion
Token typeaccess_token (opaque or JWT)access_token + id_token (JWT)
User claimsNot definedsub, email, name, etc. in id_token
ScopeCustom resource scopesopenid (required) + profile, email, etc.
SpecRFC 6749, RFC 9700OpenID 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.


ConceptRelationship
JWTThe token format most commonly used for access_token and id_token in OAuth2/OIDC
OAuth2-BFF-PatternBFF-specific implementation of Authorization Code + PKCE for browser SPAs
BFF-For-SPAEnd-to-end SPA authentication flow built on Authorization Code + PKCE
PKCEPKCE cryptographic mechanism used in Authorization Code Flow
Session-ManagementServer-side session vs stateless JWT token trade-offs after authentication
Token-Relay-PatternHow authenticated access tokens are forwarded to downstream services