What is OpenID Connect? OAuth 2.0 differences and login with authorization code + PKCE
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).
| Term | Responsibility | Common confusion |
|---|---|---|
| OAuth 2.0 | A framework for delegating API access to a client | Receiving an access token alone does not establish an authentication result intended for the app |
| OpenID Connect | Adds ID tokens and other features to OAuth 2.0 to convey user authentication | Which invoice a user may read remains an application authorization decision |
| JWT | A token format carrying claims | The 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.
| Type | Recipient and validator | Purpose |
|---|---|---|
| ID token | The RP that initiated login; aud includes its client_id | Validate the authentication result and establish an application session |
| Access token | The intended resource server (API) | Call permitted APIs; the API also validates the recipient and permissions |
| Refresh token | The issuing authorization server's token endpoint | Renew 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.
- Browser: Start login
- BFF (RP): Store transaction-specific state, nonce, and verifier bound to the initiating browser; redirect to the OP with the challenge
- IdP (OP): Authenticate the user, obtain any required consent, and return code and state to the registered callback
- BFF (RP): Check the initiating browser, state, and expiry; exchange the code with the saved verifier and client authentication
- IdP (OP): Validate the code, verifier, and other inputs; return an ID token and an access token
- BFF (RP): Validate the ID token and nonce; map iss + sub to a local user
- Browser: Receive a new application session cookie to complete login
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=S256Use 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
| Value | Where it travels and is checked | Role in this design |
|---|---|---|
| state | Sent in the authentication request; callback state is compared with the saved value | Correlate request and response; browser-bound transaction management prevents login CSRF |
| nonce | Sent in the authentication request; the validated ID token's nonce is compared with the saved value | Bind authentication to this transaction and reject tokens replayed from another transaction |
| code_verifier | Only the challenge goes in the authentication request; the verifier itself is sent during exchange | Prevent 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.
| Check | Requirement in this architecture |
|---|---|
| Required claims and format | Required OIDC fields such as iss, sub, aud, exp, and iat are present with valid types |
| Signature and alg | Verify using allowed algorithms and a trusted issuer's key; reject unsigned tokens using none in this architecture |
| iss | Exactly matches the configured issuer; separately check allowed organizations if multiple organizations share an issuer |
| aud and azp | aud includes this client_id and any additional audiences are trusted; validate azp according to the applicable extension and provider requirements |
| Time | exp has not passed; check the accepted iat range and any nbf, with a small configured clock tolerance |
| nonce | Matches the value sent for this transaction; reject missing or mismatched values and consumed transactions |
| Reauthentication requirements | Check 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 stop | Required action | What this alone does not stop |
|---|---|---|
| Application login | Invalidate the server-side session and remove its cookie | The OP's login state or separately held tokens |
| OP login | Use supported RP-Initiated Logout with a registered return URL | Other RP sessions and immediate invalidation of all issued tokens require separate confirmation |
| Future token renewal | Revoke the refresh token | Acceptance of issued access tokens by APIs using only self-contained validation |
| API access | Use short lifetimes and revocation or user-state checks as required | Stale 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 exercise | Expected result |
|---|---|
| Missing or mismatched state, or a callback started in another browser | Reject before code exchange; create no session |
| Missing or mismatched nonce, or a repeated callback | Reject token or transaction validation; create no new session |
| Exchange with the wrong verifier | The OP rejects exchange; do not fall back to a flow without PKCE |
| An ID token signed by the same OP for another client_id | Reject the audience mismatch even though the signature is valid |
| Wrong issuer, expired token, or disallowed algorithm | Create no session |
| Key rotation, or an unknown kid with failed JWKS retrieval | Accept a verifiable trusted new key; reject when no key can validate the token |
| Concurrent tab logins or parallel token renewal | Keep transactions separate and avoid concurrent use of one refresh token |
| Replay the cookie after logout, or make OP logout fail | The app rejects access; retry token revocation and tell the user if OP logout is incomplete |
| Replay a pre-logout access token at the API | Reject within the designed termination window; do not expect instant revocation from local verification alone |
Does OIDC automatically provide SSO or MFA?
- Does adding OIDC give me SSO? Reusing a common OP's authenticated session may avoid another password prompt at each RP. RP sessions and coordinated logout still require their own design.
- Does a successful login prove MFA? Authentication methods depend on OP policy. Define required assurance and authentication freshness for sensitive operations, then agree how acr, auth_time, and related claims express and verify them.
- Does using JWT mean I use OIDC? JWT is a format. Evaluate the openid request, flow with an OP, and ID token validation together to identify an OIDC login.
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.
- OpenID Connect Core 1.0, errata set 2 — ID Token, validation, UserInfo, subject identifiers
- OpenID Connect Discovery 1.0, errata set 2 — provider metadata and issuer validation
- RFC 7636 — PKCE verifier, S256 challenge, and code exchange
- RFC 9700 — OAuth 2.0 Security Best Current Practice
- RFC 8725 — JWT algorithm verification and preventing token substitution
- RFC 9068 — JWT access token profile and resource server validation
- RFC 7662 — OAuth 2.0 Token Introspection
- RFC 7009 — Token revocation and self-contained access tokens
- OpenID Connect RP-Initiated Logout 1.0 — provider logout and registered return URLs
- OWASP — Session Management Cheat Sheet
Tips this post digs into
- SPAs and mobile apps cannot keep secrets: bind the authorization code to the requesting app with PKCE
- A token with a valid signature is not necessarily meant for you: check aud and iss
- Rotate refresh tokens on every use, and revoke the whole set when a used one comes back
- Self-contained JWT verification alone does not learn about later revocation