All posts

What is OpenID Connect? OAuth 2.0 differences and login with authorization code + PKCE

· 13 min read

What must an app validate before trusting an OIDC login? Follow a web app from redirect to session creation, distinguishing OAuth 2.0 from JWT, three kinds of tokens, and state from nonce and PKCE, then design refresh and logout behavior.

What is OpenID Connect? How it differs from OAuth 2.0 and JWT

OpenID Connect gives an app a standard way to receive a user's identity as authenticated by an external identity provider. It is used when people sign in with an account from another service. The identity provider is the OpenID Provider (OP, also called an IdP); the application trusting the result is the Relying Party (RP).

TermResponsibilityCommon confusion
OAuth 2.0A framework for delegating API access to a clientReceiving an access token alone does not establish an authentication result intended for the app
OpenID ConnectAdds ID tokens and other features to OAuth 2.0 to convey user authenticationWhich invoice a user may read remains an application authorization decision
JWTA token format carrying claimsThe format itself is neither a login protocol nor an authorization policy

An OIDC request includes openid in its scope. ID tokens are JWTs, but access tokens need not be: an access token can be an opaque string. Sharing a format does not imply sharing a purpose. Signed JWT contents are normally readable, so a signature must not be mistaken for encryption that makes secret data safe to include.

ID tokens, access tokens, and refresh tokens have different jobs

Consider a hypothetical invoice web app that uses a corporate IdP for login. The app receives the login result and may call a separate invoice API. Decide who receives each token and why before implementing the flow.

TypeRecipient and validatorPurpose
ID tokenThe RP that initiated login; aud includes its client_idValidate the authentication result and establish an application session
Access tokenThe intended resource server (API)Call permitted APIs; the API also validates the recipient and permissions
Refresh tokenThe issuing authorization server's token endpointRenew access tokens and possibly other tokens without another login screen; issuance is conditional

In a general OIDC integration, do not reuse an ID token as the business API's bearer credential. Accepting an authentication result addressed to the RP can let a token cross a purpose or audience boundary. The API validates an access token intended for it, required scopes, and permission to access the particular data. A token intended for UserInfo is not necessarily usable at the invoice API.

Configure and request the business API's audience and scopes separately at the authorization server. The API validates the agreed token scheme: for opaque tokens, query validity and permissions through introspection or the supported equivalent. For JWTs, check issuer, audience, signature, expiry, and the adopted profile's token type. With RFC 9068, validate typ as at+jwt or application/at+jwt. Do not simply reuse the ID token validator.

Follow a login using authorization code + PKCE

In the authorization code flow, the browser carries a short-lived authorization code. The BFF exchanges it at the token endpoint. PKCE (Proof Key for Code Exchange) links that exchange to the initial request through a secret code_verifier created for the transaction. RFC 9700 requires PKCE for public clients and recommends it for confidential clients.

Example HTTPS BFF flow: the browser carries the code; tokens travel between the BFF and OP
  1. Browser: Start login
  2. BFF (RP): Store transaction-specific state, nonce, and verifier bound to the initiating browser; redirect to the OP with the challenge
  3. IdP (OP): Authenticate the user, obtain any required consent, and return code and state to the registered callback
  4. BFF (RP): Check the initiating browser, state, and expiry; exchange the code with the saved verifier and client authentication
  5. IdP (OP): Validate the code, verifier, and other inputs; return an ID token and an access token
  6. BFF (RP): Validate the ID token and nonce; map iss + sub to a local user
  7. Browser: Receive a new application session cookie to complete login
Illustrative authentication request parameters, not a ready-to-send URL
response_type=code
client_id=invoice-web
redirect_uri=https://app.example.com/oidc/callback
scope=openid profile email
state=<transaction-specific random value>
nonce=<a different transaction-specific random value>
code_challenge=<BASE64URL(SHA256(ASCII(code_verifier)))>
code_challenge_method=S256

Use an OIDC library for random generation, URL encoding, and response validation. A verifier is a high-entropy string of 43 to 128 characters generated per request; only its S256 challenge goes in the authentication request. Register redirect_uri for exact matching at the OP and use the same value during exchange. The profile and email scopes do not guarantee that every requested attribute will be returned.

If consent is denied or exchange fails, end the transaction without creating a session. Expired or consumed transactions must not be reused: start a new login. For a new design, use authorization code + PKCE instead of an implicit flow that returns an access token directly in the authorization response.

State, nonce, and PKCE: compare where they travel and are checked

ValueWhere it travels and is checkedRole in this design
stateSent in the authentication request; callback state is compared with the saved valueCorrelate request and response; browser-bound transaction management prevents login CSRF
nonceSent in the authentication request; the validated ID token's nonce is compared with the saved valueBind authentication to this transaction and reject tokens replayed from another transaction
code_verifierOnly the challenge goes in the authentication request; the verifier itself is sent during exchangePrevent an interceptor without the matching verifier from redeeming the code

The BFF stores state, nonce, verifier, expected issuer, redirect_uri, and expiry in a short-lived server-side record bound to the initiating browser. Check not just state equality but that this browser initiated this unused transaction. Support multiple tabs with separate records rather than overwriting one variable, and consume each transaction atomically when processing starts.

Validate the ID token: signature, iss, aud, time, and nonce

Base64URL decoding reveals JWT claims before verification, but those claims are not yet trustworthy. This architecture accepts signed ID tokens and configures an OIDC library with the expected issuer, client_id, nonce, and allowed algorithms. A JWT decoder alone is not an OIDC validator.

CheckRequirement in this architecture
Required claims and formatRequired OIDC fields such as iss, sub, aud, exp, and iat are present with valid types
Signature and algVerify using allowed algorithms and a trusted issuer's key; reject unsigned tokens using none in this architecture
issExactly matches the configured issuer; separately check allowed organizations if multiple organizations share an issuer
aud and azpaud includes this client_id and any additional audiences are trusted; validate azp according to the applicable extension and provider requirements
Timeexp has not passed; check the accepted iat range and any nbf, with a small configured clock tolerance
nonceMatches the value sent for this transaction; reject missing or mismatched values and consumed transactions
Reauthentication requirementsCheck auth_time when max_age was requested; agree the meaning of acr and related claims with the OP and reject operations that fail required MFA conditions

Fetch public keys from jwks_uri in the trusted issuer's Discovery document. Check the relationship between the configured issuer, Discovery issuer, and ID token iss. Following an arbitrary URL supplied in a token can cause trust in an attacker's key or SSRF. The kid is a hint for selecting a key within the trusted key set.

Cache JWKS to support key rotation and perform a bounded refresh for an unknown kid. If retrieval fails and no valid known key can verify the token, or the key is still missing after refresh, fail validation. Do not recover from a network failure by skipping signature verification.

Identify users by iss + sub; do not link accounts by email alone

Map the validated ID token's iss and sub together to an internal user ID. Email and display names can change or be reassigned, so they are unsuitable primary keys for persistent identity. Do not automatically merge an existing account just because an email matches; use an explicit linking process that verifies account ownership.

Pairwise identifiers can give the same person different sub values across RPs for privacy. Do not assume sub alone can join users across applications. If additional attributes require UserInfo, call it with an access token intended for UserInfo and verify that its returned sub exactly matches the ID token's sub. Before using an attribute as a tenant or permission, establish who can set it and what the OP guarantees about it.

After login: separate sessions, refresh, and logout

After validating the ID token, issue a new random session ID, replacing the preauthentication ID. Store its user mapping and expiry on the server, and set Secure, HttpOnly, and an appropriate SameSite policy on the cookie. For the top-level GET callback from a cross-site OP assumed here, verify that settings such as SameSite=Lax let the browser-binding cookie reach the callback. A POST callback changes cookie requirements.

A BFF reduces browser token exposure, but cookie-authenticated mutation APIs still need CSRF protection. HttpOnly blocks script access to cookie contents; it does not fully prevent actions through XSS. Set absolute and idle session timeouts, and check expiry, account status, and data permissions on each request. ID token exp and application session termination are different mechanisms.

Obtain a refresh token only when renewal is needed and the OP's conditions are met. Requesting offline_access does not guarantee issuance. RFC 9700 requires refresh tokens for public clients to be sender-constrained or rotated. This example also chooses OP-supported rotation for the BFF and stores tokens securely on the server.

Rotation invalidates the old refresh token at each renewal; detecting its reuse invalidates the refresh token chain from that login, or token family. Serialize concurrent refresh attempts to avoid legitimate requests triggering reuse detection against one another. If a response is lost before the replacement is saved, do not blindly resend the old token: follow the OP's recovery policy and require another login when necessary.

What to stopRequired actionWhat this alone does not stop
Application loginInvalidate the server-side session and remove its cookieThe OP's login state or separately held tokens
OP loginUse supported RP-Initiated Logout with a registered return URLOther RP sessions and immediate invalidation of all issued tokens require separate confirmation
Future token renewalRevoke the refresh tokenAcceptance of issued access tokens by APIs using only self-contained validation
API accessUse short lifetimes and revocation or user-state checks as requiredStale permissions in cached checks; include propagation time in the design

On logout, invalidate the application session first and request required revocation. If a token revocation request fails, complete local logout, record revocation as pending, and retry it from the server. OP logout requires a browser redirect: if it does not complete, show it as unconfirmed and guide the user to retry when needed. Local JWT verification does not automatically learn about immediate revocation; use introspection, a validity query to the authorization server, or server-side state checks where needed. Choose lifetimes and caches from the acceptable delay before access must stop.

Verify rejected responses as well as successful login

These are acceptance conditions for adopting this BFF design. Check them in integration tests with the chosen OP and OIDC library; they are not claims that this example has been executed as a product. Log the processing stage and failure reason, keeping codes, tokens, verifiers, and unnecessary personal data out of logs.

Condition to exerciseExpected result
Missing or mismatched state, or a callback started in another browserReject before code exchange; create no session
Missing or mismatched nonce, or a repeated callbackReject token or transaction validation; create no new session
Exchange with the wrong verifierThe OP rejects exchange; do not fall back to a flow without PKCE
An ID token signed by the same OP for another client_idReject the audience mismatch even though the signature is valid
Wrong issuer, expired token, or disallowed algorithmCreate no session
Key rotation, or an unknown kid with failed JWKS retrievalAccept a verifiable trusted new key; reject when no key can validate the token
Concurrent tab logins or parallel token renewalKeep transactions separate and avoid concurrent use of one refresh token
Replay the cookie after logout, or make OP logout failThe app rejects access; retry token revocation and tell the user if OP logout is incomplete
Replay a pre-logout access token at the APIReject within the designed termination window; do not expect instant revocation from local verification alone

Does OIDC automatically provide SSO or MFA?

Specifications and primary sources

Sources checked on October 26, 2026. OIDC Core and Discovery use errata set 2; RFC 9700 supplies current code protection and refresh token requirements. The acceptance conditions apply those sources to the BFF design in this article.

Tips this post digs into