OAuth 2.0 and OpenID Connect interviews test your understanding of delegated authorization flows and the difference between authentication and authorization.
OAuth 2.0 is a delegated authorization framework — it lets a user grant a third-party application limited access to their resources on another service without ever handing over their password. Think of a photo-printing app that needs to read your Google Photos library; instead of typing your Google credentials into that app, Google issues it a scoped access token after you approve the request. That's authorization — deciding what an app is allowed to do. Authentication is a separate concern: proving who a user is. OAuth 2.0 by itself says nothing about identity, which is exactly why OpenID Connect was layered on top of it when the industry needed a standardized way to log users in. Conflating the two is one of the most common mistakes candidates make in interviews.
The client redirects the user's browser to the authorization server's /authorize endpoint with parameters like client_id, redirect_uri, scope, and state. The user authenticates with the authorization server directly and consents to the requested scopes, so the client never sees their credentials. The authorization server redirects back to the client's redirect_uri with a short-lived, single-use authorization code. The client then makes a back-channel request to the token endpoint, exchanging that code plus its client secret, or a PKCE code_verifier, for an access token and optionally a refresh token. Because the actual token exchange happens server-to-server over a channel the browser never sees, the token itself is never exposed in a URL or browser history. This is why it's considered the most secure and recommended flow for apps that can complete a back-channel call.
PKCE, Proof Key for Code Exchange, was originally designed to protect public clients like mobile and single-page apps that can't safely hold a client secret, but it's now recommended as a baseline for essentially every authorization code flow. The client generates a random code_verifier, hashes it into a code_challenge, and sends the challenge in the initial authorization request; at the token exchange step it presents the original verifier. The authorization server hashes the verifier and checks it matches the challenge it stored earlier, proving the app redeeming the code is the same one that started the flow. This closes the authorization code interception attack, where a malicious app or network observer captures the redirect and steals the code before the legitimate client can use it. Even confidential clients benefit because it adds defense in depth against code interception. Current best practice, reflected in the OAuth 2.1 draft, essentially makes PKCE mandatory for all authorization code grants.
OAuth 2.0 is purely an authorization protocol — it produces an access token that says this app can call these APIs with these permissions, but it has no standardized concept of user identity or login. OpenID Connect, OIDC, is a thin identity layer built on top of OAuth 2.0 that adds authentication. It introduces the ID token, a JWT containing standardized claims like sub, iss, aud, and exp that vouch for who the user is and when they authenticated. OIDC also standardizes a UserInfo endpoint for fetching profile details and defines a dedicated openid scope that signals you want identity information, not just resource access. In short, OAuth answers what can this app do, OIDC answers who is this user — and most modern Login with Google or Microsoft buttons are actually OIDC flows riding on top of OAuth 2.0 infrastructure.
An access token is what the client presents to a resource server on every API call to prove it has permission to act on the resource owner's behalf; it's typically short-lived, often just minutes to an hour, to limit the damage if it leaks. A refresh token is a longer-lived credential that the client keeps privately and uses only against the authorization server's token endpoint to obtain a new access token without forcing the user to log in again. Refresh tokens are never sent to resource servers, which reduces their exposure surface compared to access tokens that travel with every request. Many implementations also rotate refresh tokens on each use, issuing a new one and invalidating the old, so a stolen refresh token can be detected if it's ever replayed after the legitimate client has already rotated it. This separation lets you keep access tokens short-lived for security while still giving users a persistent session.
The resource owner is the user who owns the data and has the authority to grant access to it. The client is the application requesting access to the resource owner's data on their behalf, like a mobile app or web app. The authorization server is the system that authenticates the resource owner, obtains their consent, and issues tokens — this is typically something like Auth0, Okta, or Google's identity platform. The resource server is where the protected data actually lives, and it validates incoming access tokens and serves the data if the token is valid and scoped correctly. In many real deployments the authorization server and resource server are separate services, but for simpler setups a single backend can play both roles.
A JWT is a compact, signed, self-contained token made of a header, a payload of claims, and a signature, and OAuth and OIDC both use it as a convenient format for tokens, though access tokens and ID tokens serve different purposes. An ID token is always a JWT per the OIDC spec, and its claims — sub, iss, aud, exp, iat, and sometimes nonce — describe the authenticated user and are meant to be consumed by the client itself, never sent to an API. An access token, by contrast, doesn't have to be a JWT at all — it can be an opaque random string that the resource server validates via introspection — but many providers issue JWT access tokens because the resource server can verify the signature locally without a network round trip. The key distinction is audience and purpose: the ID token tells the client who logged in, while the access token authorizes API calls. Mixing these up, like using an ID token to call an API, is a classic anti-pattern because ID tokens aren't scoped or intended for that audience.
Scopes are strings that define the specific, granular permissions a client is requesting, like read:contacts or write:calendar. When the client redirects the user to the authorization server, it lists the scopes it wants, and the consent screen shown to the user is built directly from that list so they know exactly what they're approving. The authorization server can grant all, some, or none of the requested scopes, and the actual granted scopes are returned alongside the token so the client knows what it has permission to do. Resource servers should also validate scopes on incoming requests, rejecting a token that has a valid signature but lacks the scope required for that particular endpoint. Good scope design follows least privilege: request only what you need, because an overly broad token is a bigger liability if it ever leaks.
The implicit flow was designed for browser-based apps that couldn't do a back-channel token exchange, so it returned the access token directly in the URL fragment after the redirect, skipping the authorization code step entirely. That design has several problems: tokens end up in browser history, referrer headers, and server logs, and there's no way to bind the token exchange to the requesting client since there's no code or verifier involved. It also doesn't support refresh tokens safely because there's no confidential back channel to redeem them over. It's more vulnerable to token injection and interception since the token is exposed on the front channel with no proof of possession. With PKCE now solving the no-client-secret problem for single-page apps, there's no scenario left where implicit flow is actually necessary. Current guidance, including the OAuth 2.1 draft, drops the implicit flow entirely in favor of authorization code with PKCE, even for pure JavaScript SPAs.
The client credentials grant is used for machine-to-machine communication where there's no human resource owner involved — think a backend service calling another backend service, or a scheduled job pulling data from an API. The client authenticates directly to the token endpoint using its own client_id and client_secret, or a stronger mechanism like a signed JWT assertion or mTLS, and in exchange it gets an access token scoped to the client's own permissions rather than any user's. There's no redirect, no browser, no consent screen, and no authorization code — it's a single, direct back-channel request. Because it grants access based purely on the client's own identity, this flow should only be used with confidential clients that can protect a secret, never from a public client like a mobile app or SPA. Refresh tokens generally aren't needed here either, since the client can just request a new token directly whenever the old one expires.
The state parameter is an opaque, unpredictable value the client generates and includes in the initial authorization request, then verifies matches when the authorization server redirects back. Its main job is preventing CSRF attacks on the OAuth callback: without it, an attacker could initiate their own authorization flow, capture the resulting code, and trick a victim's browser into completing the callback with the attacker's code, effectively binding the victim's session to the attacker's account. By checking that the returned state value matches the one it originally sent, tied to the user's current browser session, the client can be sure the response actually corresponds to a flow it initiated for that specific user. State is also commonly used to carry a small amount of app-specific context through the redirect, like the page the user was on before login. It should be stored server-side or tied to the session, not just trusted blindly from the query string.
Nonce is an OIDC-specific parameter, not part of core OAuth, and it's included in the ID token's claims rather than just echoed back in a redirect. The client generates a random nonce, sends it in the authorization request, and then checks that the same value appears inside the returned ID token's nonce claim. Its purpose is preventing ID token replay attacks: it binds a specific authentication response to the specific request that triggered it, so an attacker can't intercept a valid ID token from one login flow and replay it into a different session to impersonate the user. State protects the OAuth redirect and callback against CSRF, while nonce protects the identity assertion inside the token against replay — they solve related but distinct problems, and a properly implemented OIDC client should use both.
Token introspection, defined in RFC 7662, is how a resource server or client asks the authorization server in real time whether a given token is still valid, and if so, what metadata it carries, like its scopes, expiry, and subject. It's essential for opaque tokens that can't be validated locally, since the resource server has no way to check them without calling back to the authorization server. Token revocation, defined in RFC 7009, is the opposite operation: it's how a client or user proactively invalidates a token before its natural expiry, for example when a user logs out or a refresh token is suspected of being compromised. Once a token is revoked, subsequent introspection calls against it should report it as inactive. Together they let you handle two different production questions: is this token still good right now, and kill this token immediately regardless of when it expires.
Loose redirect URI validation is probably the biggest one — if an authorization server accepts wildcard or partial-match redirect URIs instead of exact string matching, an attacker can register a lookalike callback and steal authorization codes or tokens meant for the legitimate app. Token leakage through referrer headers, browser history, or overly verbose logging is another common issue, especially in implicit-flow-era apps that put tokens in URL fragments. Storing access or refresh tokens in localStorage on a web app exposes them to any XSS vulnerability on the page, which is why httpOnly cookies or in-memory storage are generally safer choices for browser-based clients. Missing or weak state and PKCE validation reopens the CSRF and code-interception attacks those parameters exist to prevent. And using overly broad scopes, or failing to validate the audience and issuer claims on a JWT before trusting it, means a token intended for one API can sometimes be replayed against another one entirely. Most real-world OAuth breaches come from these implementation details, not flaws in the spec itself.
An access token is an authorization artifact, not an identity assertion — it proves the bearer is allowed to perform certain actions, but it doesn't reliably tell you who the user is or guarantee it was issued for the audience checking it, especially if it's opaque or a JWT without a verified audience claim. Using it as a login credential is risky because if a resource server accepts any valid-looking access token as proof of identity without checking the intended audience, a token issued for a completely different, less trusted client could be replayed to impersonate a user there. This is the essence of the confused deputy problem: a service that has legitimate power gets tricked into misusing that power because it trusted a token it wasn't strict enough about validating. The fix is to always check the aud claim against your own client or API identifier, use ID tokens rather than access tokens for authentication, and have resource servers reject tokens that weren't specifically issued for them. This distinction between what can you do and who are you is exactly why OIDC exists as a separate layer instead of everyone hacking authentication on top of raw OAuth access tokens.