OAuth 2.0 lets a client obtain bounded authority to call a resource server. It is an authorization framework, not a login protocol. OpenID Connect adds an interoperable authentication result through ID tokens and defines discovery, UserInfo, and validation behavior. An OIDC client consumes the ID token. An API consumes an access token issued for that resource.

The distinction prevents a common failure: an API access token may be intended for another audience and does not, by itself, prove a login to the client. The client validates an ID token for its own client_id. The resource server validates an access token whose audience identifies that API.

Roles and Artifacts

ItemConsumed byWhat it meansBoundary to validate
Resource ownerAuthorization serverThe user who can approve accessAuthentication and consent policy
ClientAuthorization server and resource serverThe application requesting accessRegistered redirect URIs and client identity
Authorization serverClient and resource serverIssues tokens after policy evaluationExact issuer, signing keys, endpoints
Resource serverClientAPI holding protected resourcesAccess-token audience and scopes
Authorization codeToken endpointShort-lived, one-time intermediate resultClient, redirect URI, PKCE verifier
Access tokenResource serverDelegated authority for an audience and scopeSignature/introspection, issuer, audience, expiry
ID tokenOIDC clientAuthentication result about a subjectSignature, issuer, audience, expiry, nonce
Refresh tokenToken endpointAuthority to request replacement tokensClient binding, rotation, replay detection

Scopes bound what the client asks to do, such as calendar.read. Consent records a user’s approval where the authorization server requires it. It does not replace API-side authorization. The API must still check audience, scope, subject/tenant, and the requested resource.

Authorization Code with PKCE

Authorization Code is the current user-facing flow for web, native, and browser-based clients. PKCE binds the front-channel authorization code to a verifier generated for that authorization request.

Client -> Browser: redirect to /authorize
  response_type=code
  client_id=app-7
  redirect_uri=https://app.example/callback
  scope=openid profile calendar.read
  state=<unpredictable browser-session binding>
  nonce=<unpredictable OIDC replay binding>
  code_challenge=BASE64URL(SHA-256(code_verifier))
  code_challenge_method=S256
 
Browser -> Authorization server: authenticate user and approve requested access
Authorization server -> Browser -> Client: /callback?code=...&state=...
Client -> Token endpoint: code + exact redirect_uri + code_verifier + client authentication when applicable
Token endpoint -> Client: ID token + access token + optional refresh token
Client: validate ID token, then create its own application session
Client -> Resource server: Authorization: Bearer <access token>
Resource server: validate issuer, audience, expiry, scope, and authorization policy

The client rejects a callback whose state does not match the initiating browser session. It validates the ID token’s signature, exact issuer, audience (and azp when required), expiry, and nonce. PKCE stops an intercepted code from being redeemed without the code_verifier. It does not replace state, redirect URI validation, or client authentication.

For a server-rendered browser application, tokens can remain server-side behind a hardened opaque session cookie. A SPA or native app is a public client and cannot prove identity with a distributed static secret. PKCE and strict redirect URI validation are required controls. A backend-for-frontend can keep tokens out of browser JavaScript. Native applications can use platform-protected storage where available.

Current Flow Selector

SituationFlowWhyCost / failure boundary
Human uses web, SPA, or native appAuthorization Code with PKCEKeeps tokens out of the authorization response and binds the code to the initiating clientRequires redirects, state/nonce storage, and a token exchange
Service acts as itselfClient CredentialsNo resource owner is involved. Client receives its own bounded authorityClient credential or workload identity must be protected and rotated
TV, CLI, or input-constrained deviceDevice AuthorizationUser completes authorization on a separate browser-capable deviceDevice must poll at the specified interval and expire/stop on denial

The Implicit grant returns tokens through the authorization response and should not be used. The Resource Owner Password Credentials grant requires the client to collect the user’s password and must not be used. RFC 9700 records those current security requirements. Client Credentials is for a confidential machine client acting as itself, not a SPA.

OAuth 2.1 consolidates these restrictions and requires PKCE, but it remains an IETF Internet-Draft rather than a published RFC. The current Datatracker entry is draft 15, published in March 2026 and explicitly marked work in progress. Production guidance should therefore cite the published OAuth 2.0 RFCs and RFC 9700 while tracking the draft’s evolution.

Device Authorization Message Flow

Device -> Authorization server: client_id + requested scope
Authorization server -> Device: device_code + user_code + verification_uri + expires_in + interval
Device -> User: show URI and short code
User -> Browser -> Authorization server: authenticate and approve code
Device -> Token endpoint: poll with device_code, respecting interval
Token endpoint -> Device: access token, or authorization_pending / slow_down / denied / expired

The token endpoint’s exact RFC 8628 wire errors are authorization_pending, slow_down, access_denied, and expired_token. The sketch abbreviates the last two as denied and expired. An implementation must match the full protocol values.

The user_code is designed for typing and is not the bearer credential. The device keeps device_code secret, stops when it expires, and slows polling when instructed.

Client Credentials Example

using var client = new HttpClient();
var response = await client.PostAsync(
    "https://issuer.example/oauth2/token",
    new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["grant_type"] = "client_credentials",
        ["client_id"] = "invoice-worker",
        ["client_secret"] = configuration["OAuth:ClientSecret"]!,
        ["scope"] = "invoices.write"
    }));
 
response.EnsureSuccessStatusCode();

Prefer a managed workload identity or asymmetric client authentication over a long-lived shared secret when the authorization server supports it. Never place the secret in source control or a public client.

Failure Modes

  • Authorization response injection: bind each code to the client’s request with PKCE and reject mismatched state.
  • Redirect abuse: register exact redirect URIs and do not use an attacker-controlled continuation URL after callback.
  • Token substitution: an ID token is for the client. An access token is for the resource server. Validate each token’s intended audience.
  • Replay: validate nonce for OIDC, rotate refresh tokens, reject reuse, keep access tokens short-lived, and sender-constrain high-value tokens where supported.
  • Session confusion: federated login ends when the client creates its local session. Rotate that session at login and define local, provider, and global logout separately.
  • Token leakage: do not put tokens in query strings, logs, browser history, or analytics. TLS protects transit, not storage or logs.

Tradeoffs versus SAML

ConcernOIDCSAML 2.0
Authentication artifactJSON/JWT ID tokenXML assertion
Typical clientWeb, native, SPA, API-adjacent applicationBrowser-based enterprise application
Trust configurationIssuer discovery, client registration, redirect URIs, signing keysIdP/SP metadata, entity IDs, endpoints, signing/encryption certificates
Main implementation riskOAuth/OIDC role or token confusionXML signature/namespace handling and metadata drift
Choose it whenBuilding a new application or mobile-capable federationA required enterprise IdP or SaaS integration exposes only SAML

References