Problem and Applicable Context
Your product needs to read a third-party calendar on a user's behalf without handling that user's calendar password. Explain the complete OAuth 2.0 authorization code flow with PKCE for a public client, including what the application backend and authorization server must validate.
Assume the client is a browser single-page application or native application. It cannot reliably keep one static credential confidential across all deployed instances, so it is a public client. The authorization server authenticates the user, gathers consent, and issues tokens. The resource server exposes the calendar API. The initial scope is read-only calendar access; refresh-token issuance depends on authorization-server policy.
A public backend interview guide published in March 2026 asks candidates to explain the authorization code grant, PKCE, public and confidential clients, and the OAuth versus OpenID Connect boundary. The current security baseline is the OAuth 2.0 Security Best Current Practice: public clients must use PKCE, confidential clients are also recommended to use it, authorization servers must prevent PKCE downgrade, and registered redirect URIs require exact matching.
This is a backend question because the central skill is designing and validating a cross-service authorization protocol, token boundaries, and security controls. Browser redirects are only its transport.
What the Interviewer Is Evaluating
First, can the candidate separate the four roles? The resource owner is the user, the client is the calendar aggregation product, the authorization server handles authentication, consent, and token issuance, and the resource server owns the calendar data. Confusing the client with the user causes every later statement about permissions and token audiences to drift.
Second, can the candidate explain why the authorization code flow has two legs? The browser receives only a short-lived, single-use authorization code. The client exchanges it at the token endpoint, so the access token does not travel in a browser redirect URL. PKCE then binds a random secret created for the authorization request to that code exchange. A party that intercepts the code but lacks the code_verifier still cannot redeem it.
Third, can the candidate keep each control's responsibility precise?
| Control | Primary binding | Primary problem addressed | |---|---|---| | state | Initiating client session to callback | Request correlation and login CSRF | | PKCE | Authorization request to token exchange | Code interception and code injection | | Client authentication | Confidential client to authorization server | Identity of the client redeeming the code | | Exact redirect URI | Registered client to allowed callback | Code delivery to an attacker-controlled endpoint | | OIDC nonce | Login request to ID Token | ID Token replay and login-session correlation |
Finally, a strong answer states the boundary of PKCE. It does not stop same-origin XSS from reading a verifier or token. It does not replace TLS, redirect validation, least privilege, secure token storage, or an authentication protocol.
Clarifying Questions Before Answering
- Is the client public or confidential? An SPA or native app cannot prove identity with a
static client secret. A web application with a controlled backend can be confidential and must authenticate at the token endpoint in addition to using PKCE.
- Is the goal API authorization or product login? Calendar API access uses OAuth. If the
product needs the user's identity, use OIDC and validate an ID Token instead of inferring identity from an access token.
- Does the client use one authorization server or tenant-configured identity providers?
Multiple issuers require issuer validation or distinct redirect URIs to prevent mix-up attacks.
- Is offline access required? Do not request a refresh token when it is unnecessary. If it is
required, define rotation, replay detection, revocation, and expiry.
- Where is callback transaction state stored? Preserve a
state → code_verifierbinding.
One global verifier breaks concurrent tabs; putting it in a URL or log destroys secrecy.
- How much calendar access is needed? Start with the read-only scope required by the feature.
Write or account-wide permissions need a concrete justification.
30-Second Answer Framework
“For every authorization attempt, I generate an unpredictable state and a high-entropy codeverifier, then derive an S256 codechallenge. The browser goes to the authorization endpoint with response_type=code, client ID, an exactly registered redirect URI, minimum scope, state, and challenge. The user authenticates only at the authorization server. The callback returns a short-lived, single-use code and the same state. The client validates state, loads that transaction's verifier, and sends the code, redirect URI, and verifier to the token endpoint. The server issues an access token only after validating the code, client, redirect URI, and challenge binding. A public client cannot treat a static secret as a credential; a confidential client also authenticates. PKCE protects code exchange, state correlates the request, and an OIDC ID Token is what supports login identity.”
Step-by-Step Deep Dive
Step one: create a single-use authorization transaction.
For each “connect calendar” action, the client generates two independent random values:
stateis an unguessable transaction ID bound to the current user session, expected issuer,
callback URI, and post-authorization destination.
code_verifieris generated with a cryptographically secure random source. RFC 7636 defines it
as 43 to 128 unreserved characters and recommends at least 256 bits of entropy.
The client derives the challenge as follows:
code_challenge = base64url_without_padding(
SHA256(ASCII(code_verifier))
)
code_challenge_method = S256Store a separate state → code_verifier record for each transaction. Two browser tabs may start two calendar connections concurrently, so “the latest verifier” is not a valid data model. Give the transaction a short expiry and delete it after success or terminal failure. Do not put the verifier in redirect URLs, analytics events, or application logs.
Step two: construct the authorization request.
The client sends the user agent to the authorization endpoint:
GET /authorize
?response_type=code
&client_id=calendar-client
&redirect_uri=registered-callback
&scope=calendar.read
&state=random-transaction-id
&code_challenge=derived-challenge
&code_challenge_method=S256The authorization server validates the client ID, response type, scope, and redirect URI. The redirect URI must exactly match a pre-registered value rather than a broad domain, arbitrary subpath, or user-controlled forwarding parameter. User credentials are submitted only to the authorization server. The client receives an authorization result, never the user's password.
Step three: process the callback without calling the token endpoint yet.
After consent, the authorization server redirects the user agent to the registered callback with code and the original state. The client first validates its local transaction:
- The state exists, has not expired, and has not been consumed.
- It is bound to the current browser session and expected authorization server.
- The callback arrived at the endpoint registered for this transaction.
- An error response can still use state to find and safely terminate the correct transaction.
Abort on a state mismatch rather than “trying the token request to see whether it works.” The authorization code must be short-lived and single-use. A repeated redemption must fail, and the authorization server should revoke tokens already issued from that code when possible.
Step four: exchange the code with the verifier.
The client sends this token request:
POST /token
grant_type=authorization_code
code=returned-authorization-code
redirect_uri=registered-callback
client_id=calendar-client
code_verifier=stored-verifierThe authorization server reconstructs the original transaction: which client and redirect URI the code belongs to, whether it is expired or already used, whether the authorization request contained a challenge, and whether applying S256 to this verifier equals the stored challenge. If the authorization request lacked a challenge but the token request presents a verifier, the server must not silently downgrade the transaction to a non-PKCE flow.
A public client sends a client ID for identification, but a static secret shipped in public code cannot authenticate it. A confidential client additionally uses its registered client authentication method. PKCE supplements rather than replaces that authentication.
Step five: explain every control through an attack path.
Suppose the legitimate client creates verifier V1 and challenge C1. An attacker intercepts the callback code but does not know V1. A verifier chosen by the attacker does not produce C1, so the token exchange fails. That is PKCE's core protection against authorization code interception.
Now suppose the attacker starts a separate authorization transaction and injects that code into the victim's callback. The victim transaction has a different state and verifier. The state correlation or PKCE binding fails, and the client must abort. The authorization server must also remember whether a challenge was present for a code, preventing an attacker from deleting the challenge and triggering a downgrade.
If an attacker changes the redirect URI to an endpoint they control, exact registration and exact string matching reject it during authorization. If a client supports multiple authorization servers, it must also compare the response issuer with the issuer saved in the transaction or use a distinct redirect URI for each issuer. Remembering only an authorization endpoint URL is not enough to prevent mix-up.
Step six: restrict tokens to their real purpose.
An access token represents authorization and is not a user identity assertion for the client. The resource server verifies that the token is intended for it and limits it to the required resources and actions. The client requests only calendar.read, uses short-lived access tokens, and keeps tokens out of URLs, logs, and persistent stores available to unrelated scripts.
PKCE has completed its job after the code becomes a token. If XSS can read SPA memory, transaction storage, or an access token, PKCE cannot recover the secret. A higher-risk browser application can use a BFF: tokens remain in a controlled backend and the browser holds an HttpOnly session cookie. That reduces JavaScript token exposure but adds cookie-session, CSRF, backend scaling, and API proxy costs.
If a public client receives refresh tokens, the authorization server must detect replay through sender constraint or refresh-token rotation. With rotation, every refresh issues a new refresh token and invalidates the old one. Reuse of an old value indicates possible compromise, so the grant family is revoked and the user authorizes again.
Step seven: separate OAuth, OIDC, and other grants.
OAuth answers whether a client may access a resource on a user's behalf. OIDC adds an identity layer on top of OAuth so the client can verify a login through an ID Token. An OIDC callback also requires signature, issuer, audience, expiry, and nonce validation. An access token is intended for the resource server; an ID Token is intended for the client. They are not interchangeable.
Use client credentials when no user participates and a service accesses resources under its own authority. A constrained-input device may use a device authorization flow. The implicit flow places an access token in the authorization response, increasing URL leakage and replay exposure; current best practice favors a code-returning flow. The resource owner password credentials grant exposes the user's password to the client and is prohibited by the current security baseline.
Step eight: validate failures, not only the happy path.
| Test | Expected result | |---|---| | Change one verifier character | Token exchange fails | | Redeem the same code twice | Second exchange fails and triggers security handling | | Callback state is missing, expired, or belongs to another session | Client rejects before token exchange | | Authorization omits a challenge but token request sends a verifier | Authorization server rejects downgrade | | Redirect differs only by case, path, or trailing slash | Exact match fails | | Two tabs start flows and callbacks return out of order | Each state retrieves its own verifier | | Swap issuers after connecting two authorization servers | Client rejects the mix-up | | Scan logs and analytics | No code, verifier, access token, or refresh token appears | | XSS reads browser-accessible storage | PKCE is explicitly judged insufficient; evaluate a BFF |
Production monitoring should distinguish authorization denial, state mismatch, PKCE failure, code reuse, redirect mismatch, and refresh-token replay. A single “OAuth failed” counter hides both attack signals and client implementation defects.
High-Quality Sample Answer
“I will scope this as a public client accessing a third-party calendar API. It cannot protect a shared secret, so a secret embedded in an SPA bundle is not client authentication.
At the start of every authorization, I generate an unpredictable state and a code verifier with at least 256 bits of entropy, derive an S256 challenge, and store the verifier, browser session, issuer, redirect URI, and expiry under that state. The authorization request carries the code response type, client ID, exactly registered redirect URI, read-only scope, state, and challenge. The user authenticates and consents only at the authorization server.
When the callback returns code and state, I first prove that state belongs to the current session and is unused, then load its verifier. The token request sends the code, same redirect URI, client ID, and verifier. The authorization server verifies that the code is short-lived, unused, issued to this client and redirect URI, and that the verifier's S256 value equals the challenge before issuing an access token. An intercepted code is useless without the verifier. The server also rejects a request that tries to remove PKCE and downgrade the exchange.
State correlates the browser request, while PKCE binds the code exchange; I would not collapse them into one vague CSRF parameter. A confidential web app also authenticates at the token endpoint and should retain PKCE. The access token is only for the calendar resource server. If the product uses the flow for login, it needs OIDC and must validate the ID Token's signature, issuer, audience, expiry, and nonce.
I would test a wrong verifier, code reuse, cross-session state, exact redirect mismatch, out-of-order tabs, PKCE downgrade, and issuer mix-up, and verify that logs contain no codes or tokens. PKCE cannot stop same-origin XSS from stealing a verifier or token. For a high-risk browser client, I would evaluate a BFF that stores tokens server-side and gives the browser only an HttpOnly session cookie.”
Common Mistakes
- Treating the client ID as a secret → the client ID appears in authorization requests and
only identifies the client → **use PKCE for a public client and real client authentication for a confidential client.**
- Saying only that “a code is safer than a token” → this does not explain why an intercepted
code cannot be redeemed → show the challenge-verifier binding and token-endpoint comparison.
- Using
plainor allowing PKCE to be omitted → the verifier can be exposed or an attacker
can trigger downgrade → use S256 and make the authorization server record and enforce PKCE.
- Generating state without binding it to a session → valid values can be transplanted and
concurrent flows overwrite one another → **store a single-use state, session, issuer, and verifier mapping.**
- Allowing fuzzy redirect matching → a code can reach an attacker-controlled subpath or
redirector → pre-register and exactly compare redirect URIs.
- Decoding an access token and treating it as login identity → its audience and semantics may
belong to the resource server → use OIDC login and validate the ID Token.
- Claiming that PKCE stops XSS → XSS can read a verifier, code, or issued token directly →
reduce browser token exposure, fix XSS, and evaluate a BFF according to risk.
- Logging codes, verifiers, or tokens → diagnostics widen the exposure of redeemable secrets
→ log transaction IDs, error classes, and non-redeemable correlation values.
Follow-Up Questions and Responses
Follow-up 1: Does a traditional web app with a backend still need PKCE?
Yes. Client authentication at the token endpoint proves that a confidential client holds its protected credential. PKCE binds this authorization request to this code exchange and further protects against code injection and misuse. The controls bind different relationships. The server-side session can store the verifier and state while the browser carries only an unpredictable session identifier.
Follow-up 2: What changes if “connect calendar” also means “sign in with calendar”?
Use OIDC rather than treating an access token or arbitrary UserInfo response as login proof. Request the openid scope and nonce. Validate the ID Token signature, issuer, audience, expiry, and nonce, then use the issuer-subject pair as the external identity key. State continues to bind the browser transaction; nonce binds the authentication request to the ID Token.
Follow-up 3: What new risk appears when each SaaS tenant configures a different issuer?
The client can send a response from authorization server A to authorization server B's token endpoint, causing a mix-up. Save the expected issuer in the transaction and validate the response issuer. Another defense uses a distinct redirect URI per issuer and verifies that the response arrived at the correct endpoint. Authorization and token endpoints must come from the same trusted issuer metadata.
Follow-up 4: How do you protect refresh tokens for long-running calendar synchronization?
Grant only required scopes and use refresh-token rotation or sender constraint. With rotation, track the token family: every refresh invalidates the old value, and reuse of an old refresh token revokes the active family and requires fresh authorization. The client also needs secure storage, a revocation path, a maximum lifetime, and inactivity expiry. PKCE protects the initial code exchange, not a refresh token stolen later.
Follow-up 5: Why do two tabs connecting different calendar accounts fail intermittently?
A common cause is one global state or verifier: the second tab overwrites the first. The callback must look up an independent transaction by state. That record contains the verifier, expected account or issuer, redirect URI, and creation time. Consume it atomically. Reject unknown, duplicate, and expired states instead of falling back to the most recent verifier.