OAuth 2.0 and OpenID Connect Explained
A mobile app adds "Sign in with Provider," gets back a token, and calls it a login. OAuth 2.0 was never built to answer "who is this user" — it was built to answer "can this app access that API on the user's behalf, without ever seeing the user's password." Treating an OAuth access token as proof of identity is the single most common OAuth mistake, and it has a name for the fix: OpenID Connect (OIDC), a thin identity layer built on top of OAuth 2.0.
This guide separates what OAuth 2.0 actually delegates from what OIDC adds, follows one login through the Authorization Code flow with PKCE, and traces a real authorization-code interception to see exactly what PKCE was built to stop. For where the resulting tokens get validated on the API side, see JWT Deep Dive; for the broader session/token/OAuth model comparison, see Sessions, JWTs, and OAuth/OIDC.
OAuth 2.0 Roles: What It Actually Delegates
OAuth 2.0 defines four roles. The Resource Owner is the user. The Client is the app requesting access — a mobile app, a web app, a CLI tool. The Authorization Server authenticates the Resource Owner and issues tokens. The Resource Server is the API that accepts those tokens and serves data. OAuth's entire job is letting the Client call the Resource Server on the Resource Owner's behalf, with the Resource Owner's explicit consent, without the Client ever seeing the Resource Owner's password.
Notice what's missing from that list: nothing in OAuth 2.0 itself tells the Client who the Resource Owner is. The access token OAuth produces is scoped to calling the Resource Server's API — it was never designed to prove identity back to the Client that requested it. Early "social login" implementations that just checked whether a token could fetch a profile endpoint were repurposing an authorization protocol as an authentication one, which is exactly the gap OpenID Connect closes.
Quick reference
- Resource Owner = the user; Client = the app; Authorization Server = issues tokens; Resource Server = the API.
- OAuth's job: delegated API access with consent, without sharing the Resource Owner's credentials with the Client.
- An access token proves 'this bearer may call the Resource Server' — not 'this is definitely user X, logged into your app.'
- Scopes (e.g. read:profile, write:calendar) bound to a token define what the Client may do — narrower than 'be logged in as this user.'
- Treating a bare access token as a login credential is the historical mistake OpenID Connect exists to fix — see the OIDC section below.
Remember this
OAuth 2.0 delegates API access with the Resource Owner's consent — it was never designed to tell the Client who the Resource Owner is, which is why using a raw access token as a login proof is the classic OAuth mistake.
ID Tokens and OpenID Connect: Adding Identity
OpenID Connect adds one crucial thing on top of OAuth 2.0: the ID token, a signed JWT the Authorization Server (now acting as an "OpenID Provider") issues alongside the access token when the Client requests the openid scope. The ID token carries standardized claims — sub (a stable subject identifier for the user), iss (which provider issued it), aud (which client it was issued for), exp/iat — and it is specifically meant for the Client to establish who just logged in, unlike the access token, which is meant for calling the Resource Server.
The fix for the OAuth-as-login mistake is exactly this: validate the ID token's signature against the provider's published keys, and check that its aud claim equals your own client_id. That aud check is what closes the historical vulnerability class where a token issued to a different client (for some unrelated API scope) gets replayed against your app as if it were a login — an access token alone carries no equivalent audience guarantee your Client can rely on.
Quick reference
- openid scope is what turns a plain OAuth request into an OIDC request and triggers an id_token in the response.
- id_token claims: sub (user), iss (provider), aud (this client), exp/iat — all signed, verifiable against the provider's JWKS endpoint.
- Validate aud === your client_id before trusting an id_token — skipping this check is what makes cross-client token replay possible.
- The /userinfo endpoint (OIDC) returns additional profile claims using the access token — a separate call from validating the id_token itself.
- Access token = 'may call this API.' ID token = 'this is who logged in, to this specific client.' Do not use one for the other's job.
Remember this
OpenID Connect's ID token is signed, audience-scoped proof of who logged in to your specific client — checking its aud claim is the fix for the classic mistake of trusting a bare access token as a login credential.
Client Credentials and the Flows You Should Avoid
Not every OAuth exchange involves a Resource Owner. The Client Credentials flow is machine-to-machine: a backend service authenticates directly to the Authorization Server with its own client_id and client_secret (or a signed JWT assertion) and receives an access token scoped to itself, not to any user — the right shape for a scheduled job or service calling another service's API with no human in the loop.
Two older flows are now discouraged. The Implicit flow returned the access token directly in the URL fragment with no code-exchange step and no refresh token — convenient for early single-page apps, but it exposes tokens to browser history, referrer leakage, and any script on the page; PKCE-secured Authorization Code has fully replaced it. The Resource Owner Password Credentials flow had the Client collect the user's username and password directly and trade them for a token — which defeats OAuth's core purpose of never letting the Client see the password, and should only ever appear in a legacy migration path, never a new integration.
Quick reference
- Client Credentials: no Resource Owner, no user consent screen — service-to-service, token scoped to the calling service itself.
- Implicit flow: token in the URL fragment, no refresh token, exposed to browser history and referrer leaks — avoid for new work.
- Resource Owner Password Credentials: Client sees the raw password — avoid for new work; migrate legacy usage to Authorization Code + PKCE.
- Device Authorization flow (not covered in depth here) exists for input-constrained devices — a TV or CLI displaying a code the user enters on a second device.
- When in doubt for a new integration: Authorization Code with PKCE for anything involving a user, Client Credentials for anything that doesn't.
Remember this
Client Credentials is the correct shape for service-to-service calls with no user involved — Implicit and Resource Owner Password Credentials are legacy flows with real exposure, not options for new integrations.
Recover One Intercepted Authorization Code
Trigger. A mobile app registers a custom URL scheme (myapp://callback) as its OAuth redirect URI. A second, malicious app on the same device registers the same custom URL scheme — the operating system has no way to guarantee uniqueness the way a verified HTTPS domain does. Symptom. When the Authorization Server redirects back with the authorization code, the OS delivers it to whichever app handles that scheme, sometimes the malicious one instead of the legitimate app.
Root mechanism. Without PKCE, an authorization code alone is enough to redeem tokens at the /token endpoint — whoever intercepts the code can trade it in, no additional secret required. Recovery: because the legitimate app generated and privately held the code_verifier, the malicious app's attempt to redeem the intercepted code fails the Authorization Server's hash check — it has the code, but not the verifier that code was bound to. Verification: the Authorization Server's logs show a token-exchange attempt with a code_verifier mismatch, rejected before any token was issued. Prevention: PKCE is mandatory for all public clients per current OAuth security guidance — plus, where the platform supports it, use verified HTTPS "app links" / "universal links" as the redirect URI instead of a custom scheme, since those are cryptographically bound to one app by the OS.
Quick reference
- Trigger: a custom URL scheme redirect URI is claimable by more than one app on the same device.
- Symptom: the OS can deliver the OAuth callback to the wrong app.
- Root mechanism: without PKCE, the authorization code alone is sufficient to redeem tokens.
- Recovery: PKCE's verifier check rejects the redemption attempt from any app that didn't start the flow.
- Prevention: enforce PKCE for every public client, and prefer verified HTTPS redirect URIs over custom URL schemes where the platform supports them.
Remember this
PKCE turns an intercepted authorization code into a dead end — the verifier that redemption needs was never transmitted anywhere an interceptor could see it.
Key takeaway
OAuth 2.0 delegates API access with consent through four roles — Resource Owner, Client, Authorization Server, Resource Server — and was never designed to prove identity to the Client on its own. OpenID Connect adds that missing piece with a signed, audience-scoped ID token. PKCE closes the authorization code's biggest real-world exposure: interception on a redirect the Client doesn't fully control.
Practice (30 min): implement the Authorization Code flow with PKCE against any OAuth-compatible test provider (or a local mock authorization server). Request the openid scope, receive an ID token, and validate its signature and aud claim before trusting it as a login. Then deliberately redeem the authorization code with a wrong or missing code_verifier and confirm the server rejects it with invalid_grant. Pass when you can explain, without checking notes, why an access token alone was never enough to prove who logged in.
Related Articles
Explore this topic