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_id is public app metadata; client_secret is 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_uri controls 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, aud is the fastest clue: ID tokens usually target the client ID, while access tokens usually target an API/resource.
  • In OIDC, nonce is a random per-login value that binds the ID token back to the authentication request.
  • Token storage is a tradeoff: localStorage helps 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, SameSite cookies, 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

RoleResponsibility
Resource ownerThe user or entity granting access
ClientThe application requesting access
Authorization serverAuthenticates the user, collects consent, and issues tokens
Resource serverThe 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:

ItemWhat it meansIs it secret?Where it appears
client_idPublic identifier for the registered applicationNoAuthorization request and sometimes token request
client_secretCredential used to authenticate a confidential clientYesServer-side token request only
redirect_uriAllowed callback location after authorizationNo, but tightly validatedAuthorization request and token request
allowed scopesPermissions the client may requestNoAuthorization 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_id plus PKCE because they cannot protect a client_secret.
  • Confidential clients use client_id plus a client_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:

  1. During the authorization request, it uses client_id to load the client’s registered redirect URIs.
  2. It checks whether the requested redirect_uri is allowed for that client.
  3. 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.
  4. If the user approves, it redirects the browser to that allowed URI with code and state.
  5. During the token request, it verifies that the redirect_uri matches 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.0OpenID Connect
AuthorizationAuthentication layer on top of OAuth
Access tokensID tokens plus OAuth tokens
API scopes and delegated accessUser identity and claims
Does not define user login semantics by itselfDefines 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 sub subject claim,
  • who issued the token, the iss issuer,
  • which client the token was issued to, the aud audience,
  • 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:

ClueID tokenAccess token
Intended readerClient appAPI or resource server
aud audienceUsually the OAuth client IDUsually the API/resource identifier
Identity claimsCommon: sub, email, name, preferred_usernamePossible, but not the main purpose
API authorization claimsUsually absent or not authoritativeCommon: scope, scp, roles, permissions
OIDC claimsCommon: nonce, auth_time, acr, amrUsually absent unless provider-specific
Provider hintsMay include typ: id_token or token_use: idMay include typ: at+jwt or token_use: access
Who validates itClient validates ID tokenAPI validates access token

The strongest quick check is the audience:

  • If aud is the application’s client_id, it is probably an ID token for the client.
  • If aud is 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:

  1. The client starts a fresh login and stores nonce=n-123 locally.
  2. The user is redirected to the authorization server with that nonce.
  3. An attacker tries to inject an old ID token from a previous login.
  4. That old ID token contains an old nonce, for example nonce=n-999.
  5. The client validates the ID token and compares the token nonce to the locally stored nonce.
  6. 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:

ParameterMain purposeChecked by
stateProtects the redirect flow and carries CSRF/request correlation stateClient callback handler
nonceBinds the ID token to this authentication requestClient ID-token validation

For OIDC sign-in flows, use both.

Tokens

TokenMain useNotes
Access tokenSent to APIs as proof of authorizationOften short-lived; commonly a bearer token
Refresh tokenUsed to obtain new access tokensHigher-value secret; protect and rotate carefully
ID tokenRepresents user authentication in OIDCIntended 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.

The modern default for user-facing applications is Authorization Code Flow with PKCE.

Older patterns are less attractive:

FlowCurrent postureWhy
Authorization Code with PKCERecommended for browser, mobile, and many native clientsAvoids exposing tokens in the front channel and protects code redemption
Authorization Code with confidential client secretRecommended for trusted backend clientsBackend can protect a secret
Implicit flowDeprecated for modern SPAsTokens are returned through the browser front channel
Resource Owner Password CredentialsDeprecatedTeaches apps to collect user passwords
Client CredentialsStill valid for service-to-service accessNo user delegation involved

Where client ID and client secret are used

In Authorization Code Flow, there are two major legs:

  1. Front channel: browser redirects between the client and authorization server.
  2. 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=S256

Here, 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_value

The 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/callback

The 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_verifier

The 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:

  1. The client redirects the user to the authorization server.
  2. The user authenticates and grants consent.
  3. The authorization server redirects back with a short-lived authorization code.
  4. The client exchanges that code for tokens.
  5. 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=xyz

That 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_challenge from 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 tokens

Important PKCE terms:

TermMeaning
code_verifierThe random one-time secret generated by the client
code_challengeThe transformed value sent at the start of the flow
S256The 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 typeClient secretPKCE
SPANot safe to embedRequired
Mobile appNot safe to embedRequired
Native desktop appNot safe to embedRequired
Backend web appCan be protected server-sideStill useful as defense in depth
Machine-to-machine serviceCan use a secret or certificateUsually 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 state to protect against cross-site request forgery and response mix-up.
  • Use nonce when 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:

StorageCSRF postureXSS postureMain concern
localStorage bearer tokenStronger, because not auto-attached by browserWeaker, because readable by JavaScriptToken theft after XSS
HttpOnly cookie sessionWeaker by default, because browser auto-attaches cookiesStronger against direct JavaScript readsCSRF unless mitigated
In-memory tokenStronger against persistenceStill exposed to running XSSLost on refresh; complex refresh behavior
BFF session cookieDepends on cookie controlsKeeps OAuth tokens server-sideRequires 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:

  • HttpOnly prevents JavaScript from reading the cookie, reducing direct token theft from XSS.
  • Secure sends the cookie only over Hypertext Transfer Protocol Secure (HTTPS).
  • SameSite=Lax or SameSite=Strict tells the browser when to withhold cookies on cross-site requests.
  • SameSite=None is needed for some third-party or cross-site flows, but it requires Secure and 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=Lax

Use 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, and DELETE,
  • do not use GET for 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_id is treated as public metadata, not a secret,
  • client_secret is 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

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.