OAuth 2.0 and PKCE
OAuth 2.0 is an authorization framework. It lets an application access a protected resource on behalf of a user without asking the application to handle the user’s password.
Proof Key for Code Exchange (PKCE) is an OAuth security extension that protects public clients, such as single-page applications and mobile apps, from authorization-code interception.
Mental model: OAuth delegates access. OpenID Connect adds identity. PKCE proves that the client redeeming an authorization code is the same client that started the login flow.
Key takeaways
- OAuth is about authorization: delegated access to APIs without sharing the user’s password.
- OpenID Connect (OIDC) adds authentication: who the signed-in user is.
client_idis public app metadata;client_secretis a backend-only credential for confidential clients.- Public clients like SPAs and mobile apps cannot keep a client secret, so they use Authorization Code Flow with PKCE.
- The authorization code may briefly appear in the browser callback URL, but it is short-lived, single-use, bound to the client and redirect URI, and protected by PKCE.
redirect_uricontrols where the authorization server can send the code; apps can register multiple exact redirect URIs, but broad wildcards are risky.- Access tokens are for APIs; ID tokens are for the client to understand user identity.
- When inspecting JWTs,
audis the fastest clue: ID tokens usually target the client ID, while access tokens usually target an API/resource. - In OIDC,
nonceis a random per-login value that binds the ID token back to the authentication request. - Token storage is a tradeoff:
localStoragehelps avoid automatic CSRF but is exposed to XSS; HttpOnly cookies reduce JavaScript theft but need CSRF defenses. - Strong browser-session designs combine exact redirect URI validation,
state,nonce, PKCE, short-lived tokens,SameSitecookies, CSRF tokens where needed, and careful API token validation.
Why OAuth exists
Without OAuth, a third-party application might need the user’s password to access another service. That is a bad security boundary: the app receives too much power, password rotation becomes messy, and the user cannot easily grant narrow access.
OAuth changes the model:
- the user authenticates with the authorization server,
- the client receives scoped tokens instead of credentials,
- the resource server accepts those tokens for specific API access,
- access can be limited, expired, or revoked.
For a staff-level discussion, the important point is that OAuth is not just a login protocol. It is a delegation and authorization framework used to define trust boundaries between clients, identity systems, APIs, and users.
Core roles
| Role | Responsibility |
|---|---|
| Resource owner | The user or entity granting access |
| Client | The application requesting access |
| Authorization server | Authenticates the user, collects consent, and issues tokens |
| Resource server | The API or service that validates tokens and serves protected resources |
Example: in a Microsoft Entra ID flow, the browser app or mobile app is the client, Entra ID is the authorization server, and Microsoft Graph or an internal API is the resource server.
Client registration, client ID, and client secret
Before an OAuth flow can run, the application is usually registered with the authorization server.
That registration creates at least two important pieces of client metadata:
| Item | What it means | Is it secret? | Where it appears |
|---|---|---|---|
client_id | Public identifier for the registered application | No | Authorization request and sometimes token request |
client_secret | Credential used to authenticate a confidential client | Yes | Server-side token request only |
redirect_uri | Allowed callback location after authorization | No, but tightly validated | Authorization request and token request |
| allowed scopes | Permissions the client may request | No | Authorization request |
The client_id is like an application username. It tells the authorization server which registered app is making the request, which redirect URIs are allowed, which scopes can be requested, and which policy applies. It is expected to be visible in the browser.
The client_secret is like an application password. It proves that the token-exchange request is coming from a backend that can protect server-side credentials. It must not be embedded in JavaScript, mobile apps, desktop apps, or any downloadable client.
This creates the key split:
- Public clients use
client_idplus PKCE because they cannot protect aclient_secret. - Confidential clients use
client_idplus aclient_secret, private key, or certificate because the backend can protect credentials.
Role of the redirect URI
The redirect_uri is the callback address where the authorization server sends the browser after the user authenticates and approves or denies the request.
It has three jobs:
- route the user back to the correct app,
- tell the authorization server where it is allowed to send the authorization code,
- bind the authorization request and token exchange to the same registered callback.
An app can usually register multiple redirect URIs. Common examples:
- local development:
http://localhost:3000/callback, - staging:
https://staging.example.com/callback, - production:
https://app.example.com/callback, - mobile or native app callback: custom scheme or claimed HTTPS redirect.
The important rule is that the app should register exact, environment-specific redirect URIs. Avoid broad wildcard redirects such as https://example.com/* because they make it easier to leak authorization codes through an open redirect, forgotten route, or compromised subpath.
What the authorization server does with redirect_uri:
- During the authorization request, it uses
client_idto load the client’s registered redirect URIs. - It checks whether the requested
redirect_uriis allowed for that client. - If the redirect URI is missing, invalid, or not allowed, it should reject the request rather than send the browser to an attacker-controlled URL.
- If the user approves, it redirects the browser to that allowed URI with
codeandstate. - During the token request, it verifies that the
redirect_urimatches the one used when the authorization code was issued.
That last check matters. It prevents an attacker from starting a flow with one callback and redeeming the code with another callback. In other words, the authorization code is not just “a valid code”; it is tied to the client and redirect URI that created it.
OAuth vs OpenID Connect
OAuth 2.0 answers: What is this client allowed to access?
OpenID Connect (OIDC) answers: Who is the signed-in user?
| OAuth 2.0 | OpenID Connect |
|---|---|
| Authorization | Authentication layer on top of OAuth |
| Access tokens | ID tokens plus OAuth tokens |
| API scopes and delegated access | User identity and claims |
| Does not define user login semantics by itself | Defines identity, issuer, subject, nonce, and claims |
In practice, many “OAuth login” flows are actually OpenID Connect flows using OAuth machinery.
OpenID Connect in practice
OpenID Connect is the identity layer most product teams mean when they say “OAuth login.”
The client requests OIDC by including the openid scope in the authorization request. The authorization server then acts as an OpenID Provider and returns identity information to the client, usually through an ID token.
The ID token is a signed token that lets the client verify facts about the authentication event:
- who the user is, usually the
subsubject claim, - who issued the token, the
ississuer, - which client the token was issued to, the
audaudience, - when it was issued and when it expires,
- optional profile claims such as email or name,
- the
nonce, when used, to bind the ID token back to the login request.
The access token and ID token have different audiences:
- the ID token is for the client application to establish user identity,
- the access token is for the resource server or API to authorize access.
Do not use the ID token as an API bearer token. The API should validate an access token intended for that API audience.
Telling OIDC and OAuth tokens apart
First, be careful with wording:
- An ID token is an OpenID Connect token that represents authentication.
- An access token is an OAuth token that represents authorization to call an API.
- A refresh token is an OAuth token used to get fresh access tokens.
If the token is opaque, you cannot reliably tell much by looking at it. The authorization server or resource server has to introspect it or validate it using provider-specific rules.
If the token is a JSON Web Token (JWT), you can decode the header and payload to inspect claims. Decoding is not validation; it only lets you read the unsigned view of the token. Real validation still requires signature, issuer, audience, expiration, and key checks.
Typical clues:
| Clue | ID token | Access token |
|---|---|---|
| Intended reader | Client app | API or resource server |
aud audience | Usually the OAuth client ID | Usually the API/resource identifier |
| Identity claims | Common: sub, email, name, preferred_username | Possible, but not the main purpose |
| API authorization claims | Usually absent or not authoritative | Common: scope, scp, roles, permissions |
| OIDC claims | Common: nonce, auth_time, acr, amr | Usually absent unless provider-specific |
| Provider hints | May include typ: id_token or token_use: id | May include typ: at+jwt or token_use: access |
| Who validates it | Client validates ID token | API validates access token |
The strongest quick check is the audience:
- If
audis the application’sclient_id, it is probably an ID token for the client. - If
audis an API identifier, resource URI, or application ID URI, it is probably an access token for that API.
Provider-specific fields help, but do not rely on them universally. For example, some providers include scp, some use scope, some use roles, and some opaque tokens have no readable claims at all.
What is nonce?
nonce means “number used once.”
In OpenID Connect, the client generates a random nonce at the start of the login request and sends it to the authorization server. The authorization server includes the same value in the ID token. When the client receives the ID token, it verifies that the token’s nonce matches the value it originally generated.
The purpose is to bind the ID token to the login attempt that the client started. This helps prevent replay or token-injection attacks where an attacker tries to give the client an old or unrelated ID token.
Replay example:
- The client starts a fresh login and stores
nonce=n-123locally. - The user is redirected to the authorization server with that nonce.
- An attacker tries to inject an old ID token from a previous login.
- That old ID token contains an old nonce, for example
nonce=n-999. - The client validates the ID token and compares the token nonce to the locally stored nonce.
- The values do not match, so the client rejects the ID token.
Without a nonce check, the client may only see a validly signed ID token from the trusted issuer and accidentally accept a token that was not created for this login attempt.
The nonce must be unpredictable, tied to the current browser/session login attempt, stored until the callback completes, and cleared after validation. It should not be reused across login attempts.
nonce and state are related but different:
| Parameter | Main purpose | Checked by |
|---|---|---|
state | Protects the redirect flow and carries CSRF/request correlation state | Client callback handler |
nonce | Binds the ID token to this authentication request | Client ID-token validation |
For OIDC sign-in flows, use both.
Tokens
| Token | Main use | Notes |
|---|---|---|
| Access token | Sent to APIs as proof of authorization | Often short-lived; commonly a bearer token |
| Refresh token | Used to obtain new access tokens | Higher-value secret; protect and rotate carefully |
| ID token | Represents user authentication in OIDC | Intended for the client, not as an API authorization token |
OAuth does not require tokens to be JSON Web Tokens (JWTs). JWTs are common, especially in distributed API systems, but opaque tokens are also valid.
Recommended flow
The modern default for user-facing applications is Authorization Code Flow with PKCE.
Older patterns are less attractive:
| Flow | Current posture | Why |
|---|---|---|
| Authorization Code with PKCE | Recommended for browser, mobile, and many native clients | Avoids exposing tokens in the front channel and protects code redemption |
| Authorization Code with confidential client secret | Recommended for trusted backend clients | Backend can protect a secret |
| Implicit flow | Deprecated for modern SPAs | Tokens are returned through the browser front channel |
| Resource Owner Password Credentials | Deprecated | Teaches apps to collect user passwords |
| Client Credentials | Still valid for service-to-service access | No user delegation involved |
Where client ID and client secret are used
In Authorization Code Flow, there are two major legs:
- Front channel: browser redirects between the client and authorization server.
- Back channel: server-to-server token exchange with the authorization server.
Authorization request
The authorization request happens through the browser:
GET /authorize?
response_type=code
&client_id=app_123
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=random_csrf_value
&code_challenge=hashed_verifier
&code_challenge_method=S256Here, client_id is required because the authorization server needs to know which app is asking for access. It uses the client ID to look up allowed redirect URIs, allowed scopes, consent settings, branding, and client type.
The client_secret is not sent here. This request goes through the user’s browser, so anything in it should be treated as visible to the user and potentially logged by browser history, proxies, and monitoring systems.
Authorization response
If the user signs in and grants access, the authorization server redirects back:
https://app.example.com/callback?code=abc123&state=random_csrf_valueThe code is not the access token. It is a short-lived, single-use credential that must be exchanged at the token endpoint.
Token request for a backend web app
For a confidential backend client, the backend exchanges the code for tokens:
POST /token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=abc123
&redirect_uri=https://app.example.com/callbackThe client secret is used here because this is the server-side back channel. The authorization server authenticates the backend before issuing tokens, verifies that the code was issued to the same client, and checks that the redirect URI matches the one used in the authorization request.
Token request for a SPA or mobile app
For a public client, there is no trustworthy client_secret. Instead, the app sends the client_id and code_verifier:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=app_123
&code=abc123
&redirect_uri=https://app.example.com/callback
&code_verifier=original_random_verifierThe authorization server validates the code_verifier against the earlier code_challenge. That is the PKCE proof. It does not prove the app is confidential; it proves that the party redeeming the code is the same party that started the authorization request.
Client secret security boundary
A client secret is useful only when the client can keep it secret.
Good places for a client secret:
- backend web app,
- Backend-for-Frontend (BFF),
- server-side worker,
- machine-to-machine service,
- secure deployment environment or secrets manager.
Bad places for a client secret:
- browser JavaScript,
- mobile app bundle,
- desktop app binary,
- public Git repository,
- logs, analytics, crash reports, or frontend configuration.
If a secret is shipped to users, it should be considered public. At that point it no longer authenticates the application; it only gives attackers a reusable credential.
Operationally:
- store secrets in a secret manager or protected environment variable,
- rotate secrets after exposure or on a planned cadence,
- prefer private-key or certificate-based client authentication for higher-assurance backends when supported,
- use least-privilege scopes even for confidential clients,
- keep PKCE enabled where supported, even for backend apps, as defense in depth.
Authorization Code Flow, without PKCE
At a high level:
- The client redirects the user to the authorization server.
- The user authenticates and grants consent.
- The authorization server redirects back with a short-lived authorization code.
- The client exchanges that code for tokens.
- The client uses the access token to call the resource server.
For a backend web app, this works well because the backend can hold a client secret and exchange the code from a protected server environment.
The weakness appears with public clients.
Is the authorization code visible in the browser?
Usually, yes, briefly.
In the Authorization Code Flow, the authorization server redirects the browser back to the registered redirect URI with an authorization code, commonly like this:
https://app.example.com/callback?code=abc123&state=xyzThat means the code can be visible in the browser address bar, browser history, local development logs, reverse-proxy logs, monitoring tools, and the callback route. This is why the code must be treated as sensitive even though it is not the final access token.
The design still works because the authorization code is:
- short-lived,
- single-use,
- bound to the registered redirect URI and client,
- exchanged at the token endpoint,
- protected by PKCE for public clients.
PKCE is especially important here: if an attacker sees only the code but does not have the code_verifier, they should not be able to redeem it.
Some providers support alternate response modes such as form_post, which can avoid putting the code in the query string. That can reduce accidental logging and history exposure, but it does not remove the need for PKCE, exact redirect URI validation, state, TLS, and careful callback handling.
Why PKCE exists
Single-page applications, mobile apps, and native desktop apps cannot reliably protect a client secret. Anything shipped to a browser or installed on a device should be treated as recoverable by an attacker.
PKCE protects the authorization code exchange by adding a one-time proof:
- the client creates a high-entropy
code_verifier, - the client derives a
code_challengefrom that verifier, usually with SHA-256, - the authorization server records the challenge when the login starts,
- the client must present the original verifier when redeeming the authorization code.
If an attacker intercepts the authorization code but does not have the verifier, the code is not useful.
PKCE flow
Client creates:
code_verifier = random high-entropy secret
code_challenge = BASE64URL(SHA256(code_verifier))
Client -> Authorization server:
authorization request with code_challenge and method S256
Authorization server -> Client:
authorization code
Client -> Authorization server:
authorization code + code_verifier
Authorization server:
hashes verifier, compares it to stored challenge, then issues tokensImportant PKCE terms:
| Term | Meaning |
|---|---|
code_verifier | The random one-time secret generated by the client |
code_challenge | The transformed value sent at the start of the flow |
S256 | The SHA-256 based challenge method; prefer this over plain |
PKCE vs client secret
PKCE does not make a public client confidential. It solves a narrower problem: proving continuity between the client that started the authorization request and the client that redeems the code.
| Client type | Client secret | PKCE |
|---|---|---|
| SPA | Not safe to embed | Required |
| Mobile app | Not safe to embed | Required |
| Native desktop app | Not safe to embed | Required |
| Backend web app | Can be protected server-side | Still useful as defense in depth |
| Machine-to-machine service | Can use a secret or certificate | Usually not the main mechanism |
Staff-level framing: PKCE is not a replacement for all client authentication. It is the right control for public clients and a useful extra control for confidential clients.
Browser and frontend considerations
For modern browser apps:
- Use Authorization Code Flow with PKCE.
- Avoid implicit flow.
- Register exact redirect URIs.
- Use
stateto protect against cross-site request forgery and response mix-up. - Use
noncewhen using OIDC ID tokens. - Prefer short-lived access tokens.
- Be deliberate about refresh-token rotation and reuse detection.
- Avoid storing long-lived bearer tokens in
localStorage.
Token storage is a tradeoff:
- Memory storage limits persistence but loses tokens on refresh.
- Session storage is convenient but still exposed to successful cross-site scripting.
- HttpOnly Secure SameSite cookies protect against JavaScript token theft but reintroduce cookie and cross-site request concerns.
- Backend-for-Frontend (BFF) designs can keep tokens server-side and expose only an application session to the browser.
The right answer depends on threat model, product constraints, identity provider support, and whether the app can support a BFF.
LocalStorage, CSRF, and XSS
localStorage is relatively resistant to Cross-Site Request Forgery (CSRF) because the browser does not automatically attach localStorage values to cross-site requests. If an attacker hosts a malicious page on another origin, that page can cause the victim’s browser to send cookies to your site, but it cannot automatically attach a bearer token stored in your app’s localStorage as an Authorization header.
That does not make localStorage a safe place for long-lived tokens.
localStorage is vulnerable to Cross-Site Scripting (XSS) theft because JavaScript running in the origin can read it. If an attacker can inject script into the application, that script can read tokens from localStorage and exfiltrate them.
The tradeoff is:
| Storage | CSRF posture | XSS posture | Main concern |
|---|---|---|---|
localStorage bearer token | Stronger, because not auto-attached by browser | Weaker, because readable by JavaScript | Token theft after XSS |
| HttpOnly cookie session | Weaker by default, because browser auto-attaches cookies | Stronger against direct JavaScript reads | CSRF unless mitigated |
| In-memory token | Stronger against persistence | Still exposed to running XSS | Lost on refresh; complex refresh behavior |
| BFF session cookie | Depends on cookie controls | Keeps OAuth tokens server-side | Requires backend session design |
The staff-level answer is not “localStorage is always bad” or “cookies are always good.” It is: choose the storage model based on the threat model, then add the missing controls deliberately.
CSRF defense with HttpOnly, Secure, and SameSite cookies
Cross-Site Request Forgery (CSRF) matters most when authentication is cookie-based because browsers attach matching cookies automatically.
Cookie attributes help:
HttpOnlyprevents JavaScript from reading the cookie, reducing direct token theft from XSS.Securesends the cookie only over Hypertext Transfer Protocol Secure (HTTPS).SameSite=LaxorSameSite=Stricttells the browser when to withhold cookies on cross-site requests.SameSite=Noneis needed for some third-party or cross-site flows, but it requiresSecureand has a higher CSRF burden.
Important nuance: HttpOnly and Secure do not directly stop CSRF. The CSRF-specific attribute is SameSite, and even SameSite should often be treated as defense in depth rather than the only control.
A strong session cookie shape often looks like:
Set-Cookie: __Host-session=...; Path=/; Secure; HttpOnly; SameSite=LaxUse SameSite=Strict for highly sensitive applications when the product can tolerate stricter cross-site navigation behavior. Use SameSite=Lax when users need normal inbound links to preserve session context. Avoid state-changing GET endpoints either way.
CSRF defense with CSRF tokens
The classic CSRF defense is a server-validated anti-forgery token on state-changing requests.
Two common patterns:
- Synchronizer token pattern: the server stores a per-session or per-request CSRF token and verifies that the request includes the expected value.
- Signed double-submit cookie: the server sends a readable CSRF cookie and the client echoes the value in a request header or form field; the server verifies that the submitted value is signed and bound to the session.
CSRF tokens work because an attacker on another site can usually cause a request, but cannot read the victim’s page to learn the secret token and cannot set arbitrary custom headers without passing browser same-origin controls.
Implementation notes:
- require CSRF protection on
POST,PUT,PATCH, andDELETE, - do not use
GETfor state-changing operations, - prefer framework-provided CSRF protection when available,
- avoid putting CSRF tokens in URLs because URLs leak through logs, history, and referrers,
- add Origin or Referer checks as defense in depth for state-changing endpoints,
- remember that XSS can defeat most CSRF defenses because injected same-origin script can read or submit the token.
Design and operational checks
When reviewing an OAuth/OIDC integration, check:
- redirect URIs are exact and environment-specific,
- multiple redirect URIs are registered deliberately, not with broad wildcards,
client_idis treated as public metadata, not a secret,client_secretis used only by confidential server-side clients,- client secrets are stored in a secrets manager or protected runtime configuration,
- access tokens are audience-bound to the intended API,
- scopes are narrow and reviewed,
- ID tokens are not used as API access tokens,
- token issuer, audience, expiration, signature, and key rotation are validated,
- refresh tokens are rotated and revocable,
- logout behavior is understood across app session, identity provider session, and federated providers,
- high-risk actions can require step-up authentication,
- browser token storage has explicit CSRF and XSS tradeoff decisions,
- cookie-based sessions use
HttpOnly,Secure,SameSite, and CSRF tokens where appropriate, - observability distinguishes login failure, consent failure, token exchange failure, and API authorization failure.
At staff level, most OAuth problems are not about memorizing the flow. They are about preserving the trust boundaries across browser, backend, identity provider, API, and operational recovery paths.
Common traps
OAuth is authentication.
Not by itself. OAuth is authorization. OpenID Connect adds authentication semantics.
JWT means OAuth.
No. JWT is a token format. OAuth is a framework.
PKCE means the SPA is fully secure.
No. PKCE protects authorization-code redemption. XSS, token storage, redirect URI mistakes, weak scopes, and backend API validation still matter.
The ID token can be sent to APIs.
Usually no. APIs should receive access tokens intended for that API audience.
A decoded JWT is already validated.
No. Decoding only reads the token payload. Validation still requires checking signature, issuer, audience, expiration, and provider-specific requirements.
Implicit flow is still the best SPA flow.
No. Modern SPAs should use Authorization Code Flow with PKCE.
LocalStorage avoids all browser security problems.
No. It avoids automatic cookie attachment, which helps against CSRF, but it is readable by JavaScript and therefore exposed to XSS.
HttpOnly cookies solve CSRF.
No. HttpOnly helps against JavaScript reading cookies. CSRF needs SameSite, CSRF tokens, Origin or Referer checks, and no state-changing GET requests.
The client ID is a secret.
No. The client_id identifies the registered app but is visible in normal OAuth redirects. Security comes from redirect URI validation, PKCE, client authentication where possible, and token validation.
A client secret can be used safely in a SPA.
No. Browser JavaScript cannot keep a client secret. Use Authorization Code Flow with PKCE and treat the SPA as a public client.
A redirect URI can be any URL the app sends.
No. The authorization server should only redirect to a URI registered for that client_id, and the token request should use the same redirect URI associated with the authorization code.
Interview answer
OAuth 2.0 is an authorization framework for delegated access: the user grants a client limited access to a resource without sharing their password. In modern user-facing apps, the preferred pattern is Authorization Code Flow with PKCE, which stands for Proof Key for Code Exchange. PKCE is especially important for public clients like SPAs and mobile apps because they cannot protect a client secret. The client creates a random verifier, sends a hashed challenge during the authorization request, and later proves possession of the verifier when redeeming the authorization code. If an attacker steals only the authorization code, they still cannot redeem it. If the app also needs user identity, that is OpenID Connect on top of OAuth, using an ID token for identity and access tokens for API authorization.
References
- Auth0 - Authorization Code Flow with PKCE
- IETF - RFC 6749: The OAuth 2.0 Authorization Framework
- IETF - RFC 7636: Proof Key for Code Exchange by OAuth Public Clients
- OpenID - OpenID Connect Core 1.0
- OWASP - Cross-Site Request Forgery Prevention Cheat Sheet
- OWASP - Cross Site Scripting Prevention Cheat Sheet
- OWASP - HTML5 Security Cheat Sheet
Knowledge check - Q&A
Q: What does PKCE stand for?
Proof Key for Code Exchange.
Q: What problem does OAuth solve?
OAuth lets a client access protected resources on behalf of a user without receiving the user’s password.
Q: Is OAuth authentication or authorization?
OAuth is authorization. OpenID Connect adds authentication and user identity.
Q: What does OpenID Connect add to OAuth?
OpenID Connect adds an identity layer. It introduces the openid scope, ID tokens, standard identity claims, and validation rules so the client can verify who authenticated.
Q: Can an API use an ID token as its bearer token?
Usually no. The ID token is meant for the client. APIs should validate access tokens issued for that API audience.
Q: How can you tell an ID token and access token apart by looking?
If it is a JWT, decode the payload and check the audience and claims. An ID token usually has aud equal to the client ID and OIDC identity claims like nonce, auth_time, or user profile claims. An access token usually has aud equal to an API/resource and authorization claims like scope, scp, or roles.
Q: Can you always identify the token type by looking at it?
No. Opaque tokens cannot be understood by inspection, and JWT claim names vary by provider. Treat inspection as a clue; validation and provider documentation are the source of truth.
Q: What is nonce in OIDC?
nonce means “number used once.” It is a random value generated by the client, sent in the authentication request, and returned inside the ID token so the client can verify the token belongs to that login attempt.
Q: How does nonce prevent replay attacks?
The client stores a fresh random nonce before redirecting to the identity provider. When the ID token comes back, the client checks that the token contains the same nonce. A replayed old ID token may still be signed by the real issuer, but it contains an old nonce, so the client rejects it.
Q: How is nonce different from state?
state protects the redirect flow and helps prevent CSRF or request mix-up. nonce protects the ID token by binding it to the authentication request. In OIDC sign-in flows, use both.
Q: Why do SPAs and mobile apps need PKCE?
They are public clients and cannot safely store a client secret. PKCE lets them prove they initiated the authorization request when redeeming the code.
Q: What is the client_id used for?
The client_id identifies the registered application. The authorization server uses it to look up allowed redirect URIs, allowed scopes, consent settings, branding, and whether the client is public or confidential. It is not a password and is expected to be visible in the browser.
Q: What is the client_secret used for?
The client_secret authenticates a confidential client at the token endpoint. It proves that the token request came from a backend or service that can protect server-side credentials, not just from someone who copied the public client_id.
Q: Why is the client_secret not sent in the browser redirect?
The browser redirect is a front-channel request. URLs can appear in the address bar, browser history, logs, proxies, and monitoring tools. A secret sent there would no longer be secret.
Q: What is the redirect_uri used for?
The redirect_uri is the callback where the authorization server sends the browser after login or consent. It routes the user back to the app and controls where the authorization code is allowed to be delivered.
Q: Can one app have multiple redirect URIs?
Yes. An app can usually register multiple redirect URIs for local development, staging, production, mobile callbacks, or different app entry points. Each one should be explicit and environment-specific.
Q: What does the authorization server do with the redirect_uri?
It looks up the client by client_id, checks that the requested redirect URI is registered for that client, sends the code only to an allowed URI, and later verifies the same redirect URI during token exchange.
Q: Why are wildcard redirect URIs risky?
They expand where authorization codes can be sent. A broad wildcard can turn an open redirect, forgotten route, or compromised subpath into a way to steal authorization codes.
Q: What are code_verifier and code_challenge?
The verifier is a one-time random secret generated by the client. The challenge is the transformed value, usually SHA-256 based, sent at the start of the flow.
Q: What happens if an attacker steals the authorization code?
With PKCE, the attacker still needs the verifier. Without it, the authorization server should reject the token exchange.
Q: Is the authorization code visible in the browser address bar?
Usually yes, briefly, when the authorization server redirects to a callback URL like /callback?code=...&state=.... The code is still protected by short lifetime, single use, redirect URI validation, state, TLS, and PKCE.
Q: Does PKCE replace all need for client authentication?
No. It replaces embedded client secrets for public clients and adds defense in depth for some confidential clients. Backends can still use client secrets, private keys, or certificates.
Q: Why does a backend use a client secret?
A backend is a confidential client: it can keep secrets on the server instead of shipping them to a browser or mobile device. The client secret lets the authorization server authenticate the backend during token exchange, proving that the request comes from the registered application and not just from someone who copied the public client_id.
Q: Does a backend still need PKCE if it has a client secret?
The client secret authenticates the backend. PKCE protects the authorization code from interception and replay by requiring the original verifier. Many systems use both for defense in depth.
Q: What happens if a client secret leaks?
Treat the client as compromised: rotate the secret, audit token issuance and redirect URI settings, review logs for abuse, and consider whether tokens or refresh tokens need revocation.
Q: Why is implicit flow discouraged?
It exposes tokens through the browser front channel. Authorization Code Flow with PKCE avoids that pattern and gives the authorization server a safer token exchange point.
Q: Where should browser apps store tokens?
There is no universal answer. Memory is safer against persistence but worse for refresh. HttpOnly cookies can reduce JavaScript theft but need CSRF controls. For high-risk apps, a BFF that keeps tokens server-side is often cleaner.
Q: Why is localStorage resistant to CSRF?
Cross-site attackers cannot make the browser automatically attach localStorage values to requests. A bearer token in localStorage has to be read by same-origin JavaScript and explicitly sent in a header.
Q: Why is localStorage vulnerable to XSS?
Cross-Site Scripting means attacker-controlled JavaScript runs in the application’s origin. That script can read localStorage and steal bearer tokens.
Q: Do HttpOnly and Secure cookies prevent CSRF?
Not by themselves. HttpOnly protects against JavaScript reads, and Secure restricts the cookie to HTTPS. CSRF protection comes from SameSite, CSRF tokens, Origin or Referer checks, and avoiding state-changing GET routes.
Q: What does SameSite do?
SameSite tells the browser whether to include a cookie on cross-site requests. Strict is strongest but can hurt cross-site navigation. Lax is a common balance. None allows cross-site cookie sending and must be paired with Secure.
Q: How does a CSRF token defend cookie-based sessions?
The server requires a secret, unpredictable token on state-changing requests. An attacker can cause a cross-site request, but usually cannot read the real page or set the required custom header with the correct token.
Q: Can XSS defeat CSRF tokens?
Yes. If attacker JavaScript runs in the application’s origin, it can often read or submit the CSRF token. XSS prevention remains a prerequisite for trustworthy CSRF defenses.
Q: What is the most important staff-level review question?
Whether the system preserves clear trust boundaries: who can initiate login, who can redeem codes, which API each token is meant for, where tokens live, how they expire, and how access is revoked.