Skip to content

OAuth 2.0 and OpenID Connect Security Engineering

Application Security 7 min read

Evidence status

Implementation
Partially tested
Review
Partially verified
Sources
Primary sources reviewed
Last reviewed
What do these statuses mean?

Partially tested: Some controls are backed by executable tests, but the complete architecture is not production-validated.

Partially verified: The article has reviewed evidence, but one or more documented validation gaps remain.

Read the research methodology

OAuth delegates access; OpenID Connect (OIDC) adds authentication and identity claims. A secure implementation validates the right token for the right consumer, binds authorization responses to the initiating session, uses exact redirect URIs, protects authorization codes with PKCE, limits bearer-token replay, and treats the authorization server's metadata and keys as trusted configuration - not arbitrary input.

RFC 9700 is the current OAuth Security Best Current Practice. It replaces many older recommendations and makes the authorization code flow with PKCE the general browser/ native baseline. The implicit grant should not be used for new designs.

Roles, artifacts, and audiences

Artifact Intended consumer Essential interpretation
Authorization code Client's registered redirect endpoint Short-lived intermediate value; redeem once with redirect/client binding and PKCE verifier
ID token OIDC client Authentication statement about the end-user session; audience normally includes the client's client_id
Access token Resource server/API Authorization credential; audience/resource indicator must identify the API that accepts it
Refresh token Authorization server token endpoint Long-lived delegation handle; protect from replay and bind/rotate as required

Do not send an ID token to an API as an access token. Do not validate every access token by comparing aud to a UI client's ID: the resource server validates that the token is intended for that resource/API. Exact access-token format and validation may be JWT-profile based, opaque/introspection based, or provider-specific; configure one supported contract rather than guessing from token shape.

Threat model

sequenceDiagram
  participant U as User agent
  participant C as OIDC client
  participant AS as Authorization server
  participant API as Resource server
  C->>C: Create state, nonce, PKCE verifier/challenge
  C->>AS: Authorization request with exact redirect and challenge
  AS->>U: Authenticate and authorize
  AS->>C: Code plus state (and issuer where applicable)
  C->>C: Validate state and expected issuer
  C->>AS: Redeem code with verifier and client authentication if confidential
  AS->>C: ID token, access token, optional refresh token
  C->>C: Validate ID token issuer, audience/client, signature, time, nonce
  C->>API: Access token
  API->>API: Validate issuer, API audience, signature/introspection, time, authorization

Material threats are redirect interception, authorization-code injection, CSRF, authorization-server mix-up, open redirectors, token substitution, bearer replay, refresh-token theft, algorithm/key confusion, discovery/JWKS SSRF, excessive scopes, and incorrect subject/account linking.

Authorization request and callback

Exact redirect registration

Authorization servers must compare redirect URIs using exact string matching as specified by RFC 9700, with the native-app loopback exception defined by the native application BCP. Avoid wildcard hosts/paths and open redirectors in clients and authorization servers. Validate post-login application return locations separately against a narrow allowlist; never copy an arbitrary returnUrl into an OAuth redirect.

PKCE

Public clients must use PKCE, and RFC 9700 recommends it for confidential clients as well. Generate a high-entropy verifier per authorization request, store it bound to the initiating browser/session, send the S256 challenge, and delete it after one redemption attempt. Reject downgrade/missing verifier. PKCE protects authorization code interception/injection; it does not replace client authentication for a confidential client or validate the user session by itself.

state, nonce, and issuer

  • Bind state to the initiating user-agent session and request details; compare once using a safe equality operation and expire it. It is a correlation/CSRF mechanism, not a place for unsigned trusted application state.
  • For OIDC, generate and validate nonce in the ID token to bind authentication to the request and resist replay.
  • Defend mix-up by binding the expected authorization server/issuer to the transaction and validating the authorization response issuer mechanism (RFC 9207) or the applicable OIDC response semantics.

Avoid putting sensitive business data in front-channel parameters. Signed/encrypted request objects and pushed authorization requests (PAR, RFC 9126) reduce front-channel tampering/disclosure and are required by stronger profiles such as FAPI 2.0 in their specified combinations.

Token validation

Use a maintained library and configured issuer metadata. Pseudocode for an ID token:

# Pseudocode: library calls and provider requirements vary.
metadata = cached_metadata_for(configured_issuer)
key = select_allowed_key(metadata.jwks, token.header.kid)
reject unless header.algorithm is in the configured asymmetric allowlist
claims = verify_signature_and_decode(token, key)
require claims.iss == configured_issuer
require client_id is an allowed member of claims.aud
validate azp according to OIDC Core when multiple audiences are present
validate exp, iat, and nbf with narrow documented clock skew
require claims.nonce == one_time_session_nonce

The resource server performs a separate access-token validation for itself:

# Pseudocode: JWT access token profile or introspection contract must be explicit.
claims = validate_access_token(token, configured_authorization_server)
require api_resource_identifier is in claims.aud
require token is active and within time bounds
require scopes/authorization_details and subject/client are allowed for this action
enforce tenant/object/business authorization using server-side data

Reject alg: none, algorithms outside a narrow allowlist, tokens signed by the wrong key type, missing required claims, unknown critical headers, and keys from an unconfigured issuer. kid selects among already trusted issuer keys; it is not a URL to fetch. Fetch discovery/JWKS only from a configured HTTPS issuer with controlled redirect, DNS, egress, size, timeout, cache, and key-rotation behavior.

Key cache logic must handle legitimate rotation without accepting an arbitrary new issuer or refreshing on every attacker-controlled kid. Keep last known-good keys for a bounded overlap according to provider behavior, refresh with rate limits, and fail closed when no trusted key validates.

Sender-constrained access tokens

Bearer tokens can be replayed by whoever steals them. For higher-risk APIs, evaluate:

  • OAuth mutual TLS (RFC 8705), which binds tokens to a client certificate and requires end-to-end certificate identity/proxy handling and lifecycle operations;
  • DPoP (RFC 9449), which binds tokens to a proof key and requires request proof, method/URI binding, nonce/replay controls, clock handling, and secure client key storage.

Sender constraint narrows replay but does not repair excessive authorization, client compromise, or a resource server that fails to validate the binding.

Refresh tokens and sessions

RFC 9700 requires public-client refresh tokens to be sender-constrained or use refresh token rotation so replay is detected. Confidential clients also need secure storage, client authentication, narrow scope/audience, inactivity/absolute expiry according to risk, revocation, and reuse detection. On reuse, revoke the affected token family or take provider-defined containment action and investigate the client session.

Browser applications should prefer a backend-for-frontend or another architecture that avoids exposing durable tokens to browser JavaScript when it fits the product. Cookies need Secure, HttpOnly, appropriate SameSite, CSRF controls, rotation at authentication/privilege change, bounded lifetime, and logout/revocation behavior.

Client authentication and secrets

Public clients cannot keep a static secret. Do not ship one in a mobile, desktop, or single-page application. Confidential clients should prefer stronger asymmetric methods (private-key JWT or mTLS when profile/provider support fits) over a widely copied shared secret, and must manage keys as production credentials.

Workload federation can remove stored cloud credentials but is separate from end-user OAuth. Validate issuer, audience, subject, and resulting authorization in both cases.

Authorization design

Scopes express delegated capability but rarely encode complete object/tenant/business authorization. The API must evaluate token subject/client, scopes or authorization details, current account/tenant membership, resource ownership, consent/policy, and request context. Avoid stable broad refresh/access grants when a narrower resource indicator, incremental consent, or transaction-specific authorization is available.

Use provider pairwise subjects/issuer+subject as account keys according to the protocol. Do not link accounts only by unverified email. Validate claims such as email_verified only within the provider's documented semantics and business policy.

Failure modes and operations

  • Discovery/JWKS unavailable: use bounded valid cache; do not skip signature validation or accept another issuer.
  • Clock skew: monitor time synchronization and use a narrow documented leeway; do not grant long arbitrary grace periods.
  • Authorization callback mismatch: terminate the transaction, clear one-time state, and log a safe correlation event.
  • Refresh reuse: revoke/contain the family, require reauthentication as appropriate, and investigate device/client telemetry.
  • Provider compromise/misconfiguration: disable trust, revoke sessions/tokens where possible, freeze account linking, and use preplanned alternate authentication or recovery - not local password fallback created during the incident.

Log issuer, client ID, API audience, subject pseudonym/internal principal, scopes, authorization outcome, token/session identifier hash, reason code, and trace ID without logging raw tokens/codes/verifiers. Alert on issuer/audience/algorithm mismatch, refresh reuse, callback errors, abnormal consent/scope, discovery/key churn, and token use inconsistent with sender constraint.

What I actually ran

The linked OAuth/OIDC token-boundary lab was run locally on 2026-07-23:

Runtime What I actually ran Result
Node.js 24.12.0 RS256 signature, issuer, string/array audience, expiration, nbf, tenant, scope, bounded key rotation, unknown kid, and exact web redirect matching 14 of 14 positive and negative cases passed
Go 1.26.1 RFC 7636 S256 vector and generated round trip, with wrong verifier, plain downgrade, method case, verifier length/syntax, and malformed-challenge rejection 8 of 8 cases passed

The Node code is a dependency-free executable model with injected time and in-memory trusted keys. The Go code demonstrates the PKCE verifier boundary and does not log verifiers or challenges. These checks back up the specific invariants above; they don't prove that a real OAuth/OIDC library, provider, discovery flow, JWKS transport, session store, deployment, or full authorization design would hold up in production.

Validation checklist

Authorization code with S256 PKCE is used; implicit is absent.

Redirect URIs are exact and application return URLs are separately allowlisted.

State, nonce, expected issuer, and one-time transaction data are validated.

ID token audience is the client; access-token audience is the accepting API.

Signature algorithm, issuer, keys, time, and required claims are allowlisted.

Discovery/JWKS retrieval cannot become arbitrary SSRF.

Public-client refresh tokens rotate or are sender-constrained.

Scopes do not replace tenant/object/business authorization.

Token values are never logged and failure paths are observable.

PAR/FAPI/sender constraint is evaluated for high-value APIs.

Limitations

Providers add profile-specific claims, logout, token exchange, device authorization, dynamic registration, and key-rotation behavior. Validate the provider's conformance and current documentation. Pseudocode is intentionally not a drop-in token validator. The executable model does not perform discovery, network JWKS retrieval, provider integration, library interoperability, session persistence, or deployment validation.

References