Rapid-Fire: OAuth 2.0 and OpenID Connect
Where candidates most often reveal they have used a library without understanding what it does.
What problem does OAuth solve?
Letting an application act on a user's behalf against an API without the user handing over their password. The result is a scoped, expiring, revocable token instead of a credential that grants everything forever.
Is OAuth authentication?
No. OAuth 2.0 is delegated authorization. An access token says "the bearer may read Alice's calendar"; it does not say "this is Alice", and it does not say who it was issued to. OpenID Connect adds authentication with the ID token, whose aud names your client id.
Why the authorization code flow rather than getting a token directly?
Because the code travels through the browser's URL, and URLs leak into history, referrers and logs. The code is single-use, expires in about a minute, and is worthless without the client secret or the PKCE verifier. The valuable token only ever moves over a direct server-to-server POST.
What is the state parameter for?
CSRF protection on the callback. Without it, an attacker completes their own authorization, then lures the victim to the callback carrying the attacker's code — silently linking the victim's session to the attacker's account. It is the CSRF token of the OAuth flow.
What does PKCE do, and who needs it?
It replaces the client secret for public clients. The client sends SHA256(verifier) to /authorize and the verifier itself to /token, so an intercepted code cannot be redeemed. OAuth 2.1 makes it mandatory for every client, confidential ones included.
Why were implicit and password grants removed?
Implicit put the access token in the URL fragment with no client authentication and no refresh token. ROPC had the application handle the user's password, which defeats OAuth's purpose and makes MFA impossible. Both are replaced by authorization code + PKCE.
The trap question: I have an access token from Google — can I log the user in with it?
No. That is the confused-deputy vulnerability. An access token has no audience naming your client, so a token any other application obtained for that user would be accepted at your login endpoint. Use the ID token, verified, with aud equal to your client id.
Example
// The trap, and the answer
// ❌ Confused deputy — full account takeover
app.post('/auth/google', async (req, res) => {
const profile = await fetchGoogleProfile(req.body.accessToken);
await loginAs(profile.email);
});
// A malicious app collects an access token for ITS OWN scopes, posts it here,
// and is logged in as that user. The token never named you.
// ✅ Verify the ID token, whose audience IS you
const claims = await jwtVerify(idToken, GOOGLE_JWKS, {
algorithms: ['RS256'],
issuer: 'https://accounts.google.com',
audience: process.env.GOOGLE_CLIENT_ID, // ← the whole defence
});
if (claims.nonce !== tx.nonce) throw new Error('nonce_mismatch');
await loginAs(claims.sub); // key on sub, never on email aloneWhen to use it
- A candidate is asked to add 'Sign in with Google' on a whiteboard and correctly ends the flow by issuing the application's own session.
- An interviewer probes account linking and the candidate names the email_verified requirement before being prompted.
- A discussion about mobile OAuth leads naturally to PKCE because the candidate raises the custom-URL-scheme interception problem.
More examples
Draw the flow, then annotate the security of each step
Annotating each arrow with what it defends against turns a memorised sequence into evidence that you understand why the sequence exists.
1. GET /authorize?client_id&redirect_uri&scope&state&code_challenge&S256
↳ state : CSRF protection on the callback
↳ code_challenge : PKCE — binds the future exchange to this client
↳ redirect_uri : matched EXACTLY against a registered value
2. user authenticates AT THE PROVIDER ← your app never sees the password
3. user consents to the listed scopes ← least privilege, incrementally
4. 302 → https://abc.com/callback?code=…&state=…
↳ the code is single use and lives ~60 seconds
↳ it is worthless without the secret or the PKCE verifier
5. verify state matches what we stored ← STOP HERE if it does not
6. POST /token (back channel, server to server)
code + client_secret + code_verifier
↳ nothing valuable ever travelled through the browser
7. verify the ID TOKEN: signature, iss, aud=our client_id, exp, nonce
8. issue OUR OWN session
↳ we control lifetime, revocation and MFA policy — not the provider
# Step 8 is the one candidates omit, and interviewers always ask about it.Which grant, and why — asked as a scenario
The last line catches people out. Candidates often say 'never send a password to an endpoint', which over-applies the rule and shows it was memorised.
"A React SPA needs to call our API."
→ Authorization Code + PKCE. No client secret exists in a browser.
Follow-up: "Why not implicit?" → token in the URL, no PKCE, no refresh.
"A mobile app."
→ Same, in the SYSTEM browser (ASWebAuthenticationSession / Custom Tabs),
never an embedded webview — a webview lets the app read the credentials.
PKCE is essential because another app may claim the same URL scheme.
"A nightly job syncing data."
→ Client credentials. No user, so no consent and no user identity.
Better: private key JWT, or workload identity so no secret exists at all.
"A CLI on a headless server."
→ Device authorization flow. Print a code, the engineer approves it on a
laptop, the CLI polls — respecting `interval` and `slow_down`.
"Another company's product acting for our users."
→ Authorization Code + PKCE with narrow scopes and a consent screen.
Never share credentials — that is the whole reason OAuth exists.
"Our own login form."
→ Not OAuth at all. Posting email and password to your OWN endpoint is
fine; ROPC means handing credentials to a party that is not the
owner-of-record. Being able to draw that line is the signal here.Follow-ups that go beyond the happy path
These are the questions that follow a correct happy-path answer. Having them ready is what turns 'knows OAuth' into 'has operated OAuth'.
Q: "The user revokes access from their Google account page. What happens?"
A: Our refresh token stops working at the next refresh; existing access tokens
remain valid until they expire. If we need faster, subscribe to the
provider's revocation signals (CAEP / shared signals) rather than polling.
Q: "Two providers, and the same email address. What do you do?"
A: Key accounts on (provider, sub) — never on email alone. Link automatically
ONLY when the provider reports email_verified; otherwise require a password
confirmation. Auto-linking an unverified email is an account takeover.
Q: "Where do you keep the provider's access token?"
A: Server-side, and only if we will actually call their API. If we only wanted
identity, discard it after login. An unused stored token is pure liability.
Q: "redirect_uri matching — why must it be exact?"
A: A relaxed match lets an attacker redirect the code to a host they control.
Prefix or wildcard matching on redirect URIs is a known bypass class.
Q: "What's the difference between scope and role?"
A: Scope is what the CLIENT was permitted to request; role is what the USER may
do. Both apply, and the effective permission is the intersection. Checking
only one leaves a privilege escalation.
Q: "Your OAuth provider is down. What breaks?"
A: New logins. Existing sessions continue because we issued our own. That is
the reason step 8 exists, and it is why we do not proxy every request to
the provider.
Discussion