OAuth 2.0 vs OpenID Connect: Authorization vs Authentication
OAuth 2.0 vs OpenID Connect is the single most confused comparison in application security, and the confusion is expensive: teams routinely use OAuth 2.0 to log users in — a job it was never designed to do — and end up with subtly broken, insecure sign-in. The short version is that OAuth 2.0 is an authorization framework (it grants an app access to an API on a user's behalf), while OpenID Connect (OIDC) is a thin identity layer on top of OAuth 2.0 that answers a different question: who is this user? This guide is OAuth 2.0 vs OpenID Connect explained for engineers who build and secure real systems — what each one actually does, the tokens and flows involved, and how to pick the right one. It is a supporting page for the Devgains security cluster and its zero trust pillar, where identity is the foundation that replaces the network perimeter.
Quick answer: what is the difference between OAuth 2.0 and OpenID Connect?
OAuth 2.0 handles authorization; OpenID Connect handles authentication. They are not competitors — OIDC is built on OAuth 2.0 and reuses its flows.
- OAuth 2.0 answers "is this app allowed to do X on the user's behalf?" It issues an access token that an app presents to an API. It says nothing reliable about who the user is.
- OpenID Connect answers "who is this user, and when/how did they authenticate?" It adds an
ID token (a signed JWT with identity claims) and a standard
/userinfoendpoint.
One line to remember: OAuth gets you a key to an API; OIDC gets you a verified identity. If you are building "Sign in with Google," you want OIDC. If you are letting an app read a user's calendar, you want OAuth 2.0.
Why the distinction matters
Using OAuth 2.0 for login is a classic anti-pattern. An OAuth access token is a bearer credential for an API — it is opaque to the client, has no defined format, and carries no guarantee about the end user. Teams that treat "the app received an access token" as "the user is authenticated" open themselves to real attacks:
- Token substitution / confused deputy. An access token minted for one client can sometimes be replayed to another. Because OAuth never intended the token to prove identity, there is no audience check protecting you.
- No
nonce, no replay protection for login. OIDC's ID token binds the response to your original request; raw OAuth has no equivalent for authentication. - Guessing identity from the wrong place. Reading a user id out of an access token or calling a provider-specific "me" endpoint is non-standard and breaks across providers.
This is exactly the mindset zero trust demands: never trust, always verify. Identity must be proven with a token designed to prove identity — that is what the ID token is for.
Architecture: how OIDC layers on OAuth 2.0
OAuth 2.0 defines four roles: the resource owner (the user), the client (your app), the
authorization server (issues tokens), and the resource server (the API). OIDC keeps all of
these and adds one concept — the ID token — plus a discovery document and a /userinfo
endpoint.
User (Resource Owner)
| authenticates & consents
v
Authorization Server --- issues ---> access_token (OAuth: call the API)
(Identity Provider) --- issues ---> id_token (JWT) (OIDC: who the user is)
|
v
Your App (Client) --- access_token ---> Resource Server (API)
--- verifies ------> id_token signature + claimsThe tokens do different jobs and must not be swapped:
- Access token → sent to the API. Your app treats it as opaque and never inspects it.
- ID token → consumed by your app. It is a JWT you do verify (signature, issuer, audience, expiry, nonce) to establish the user's session. Related reading: where you keep tokens matters.
Step-by-step: an OIDC Authorization Code flow with PKCE
The modern, recommended flow for both web and native apps is Authorization Code with PKCE (Proof Key for Code Exchange). It never exposes tokens in the browser URL and protects against authorization-code interception.
1. The app builds an authorization request. Note the scope=openid — that single scope is what
turns plain OAuth into OIDC and asks for an ID token.
# 1) Redirect the user to the authorization server
GET https://idp.example.com/authorize
?response_type=code
&client_id=web-app-123
&redirect_uri=https://app.example.com/callback
&scope=openid profile email # 'openid' => request an ID token (OIDC)
&state=xyz # CSRF protection, echoed back
&nonce=n-0S6_WzA2Mj # binds the ID token to THIS request
&code_challenge=E9Melhoa... # SHA-256 of a random verifier (PKCE)
&code_challenge_method=S256The code_challenge is the SHA-256 hash of a random code_verifier your app generated and kept
secret. state blocks CSRF on the callback; nonce will be embedded in the ID token so you can
detect replay.
2. Exchange the code for tokens. After the user authenticates and consents, the server redirects
back with a short-lived code. Your app swaps it — presenting the original code_verifier — for
tokens.
# 2) Back-channel token exchange (PKCE proves you started the flow)
POST https://idp.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQ
&redirect_uri=https://app.example.com/callback
&client_id=web-app-123
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkThe response contains both tokens plus a refresh token:
{
"access_token": "opaque-or-jwt-for-the-API",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "8xLOxBtZp8"
}3. Verify the ID token — do not skip this. The ID token is a JWT. Your app must validate it before trusting a single claim.
import { jwtVerify, createRemoteJWKSet } from "jose";
// Public keys are published at the provider's JWKS endpoint (from discovery).
const JWKS = createRemoteJWKSet(new URL("https://idp.example.com/jwks"));
async function verifyIdToken(idToken: string, expectedNonce: string) {
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: "https://idp.example.com", // 'iss' must match your provider
audience: "web-app-123", // 'aud' must be YOUR client_id
});
// Replay protection: the nonce must match the one you sent in step 1.
if (payload.nonce !== expectedNonce) throw new Error("nonce mismatch");
// 'sub' is the stable, unique user id. Prefer it over email as the key.
return { userId: payload.sub, email: payload.email };
}The four checks that matter every time: signature (via JWKS), issuer (iss), audience
(aud must equal your client_id), and expiry (exp) — plus the nonce for login flows.
Skip any of these and the ID token stops being trustworthy.
OAuth 2.0 vs OpenID Connect: comparison table
| OAuth 2.0 | OpenID Connect (OIDC) | |
|---|---|---|
| Primary purpose | Authorization (delegated access) | Authentication (identity) |
| Question it answers | "Can this app do X?" | "Who is this user?" |
| Core token | Access token | ID token (JWT) + access token |
| Token audience | The API (resource server) | The client (your app) |
| Token format | Opaque or JWT (undefined) | Signed JWT with standard claims |
| Standard user info | None | id_token claims + /userinfo |
| Identity guarantees | None | iss, sub, aud, exp, nonce |
| Typical use | "Allow app to read your calendar" | "Sign in with Google/Microsoft" |
The rule of thumb: if a human is logging in, you need OIDC. If a machine or app is being granted scoped access to an API, OAuth 2.0 is enough.
Best practices
- Use Authorization Code + PKCE for all interactive apps — web and native. The implicit flow is deprecated; do not use it.
- Add the
openidscope and verify the ID token whenever you need login. Do not infer identity from an access token. - Validate
iss,aud,exp, signature, andnonceon every ID token, using the provider's JWKS for key rotation. - Key users on
sub, not email. Emails change and can be reassigned;subis the stable identifier for that user at that provider. - Keep access tokens short-lived and use refresh tokens (rotated) for longevity — aligned with short-lived credentials in zero trust.
- Protect the client secret. Confidential clients store it server-side; never ship it to a browser or mobile binary. See secrets in environment variables.
Common mistakes
- Using OAuth 2.0 access tokens to log users in. The token is for the API, not for proving identity. Request an ID token via OIDC instead.
- Not validating the ID token audience (
aud). Without the audience check, a token minted for another client can be replayed against yours — a broken auth bug waiting to happen. - Skipping
stateandnonce.statestops CSRF on the redirect;noncestops ID-token replay. Both are cheap and mandatory. - Using the implicit flow. Returning tokens in the URL fragment leaks them to history, logs, and referrers. Use Authorization Code + PKCE.
- Treating scopes as roles. OAuth scopes gate API access, not in-app authorization. You still need your own access-control checks on the resource server.
Takeaways
- OAuth 2.0 = authorization; OpenID Connect = authentication. OIDC is a layer on OAuth, not a replacement.
- The access token goes to the API and is opaque to your app; the ID token is a JWT your app verifies to establish who the user is.
- Adding
scope=openidand validating the returned ID token is the difference between real login and a fragile hack. - Use Authorization Code with PKCE everywhere; the implicit flow is dead.
- Always verify signature,
iss,aud,exp, andnonce— that is what makes identity trustworthy under zero trust.
FAQ
What is the difference between OAuth 2.0 and OpenID Connect? OAuth 2.0 is an authorization framework that lets an app access an API on a user's behalf via an access token. OpenID Connect is an authentication layer built on OAuth 2.0 that adds an ID token — a signed JWT of identity claims — so an app can verify who the user is. Use OAuth for delegated API access and OIDC for login.
Can I use OAuth 2.0 for authentication?
Not safely on its own. OAuth access tokens are meant for API access and carry no reliable identity
guarantees, so using them to log users in leads to token-substitution and replay problems. Add the
openid scope to get OpenID Connect, which provides an ID token designed for authentication.
What is an ID token vs an access token? An access token is a credential your app sends to an API; your app treats it as opaque. An ID token is a signed JWT your app itself validates (signature, issuer, audience, expiry, nonce) to establish the user's identity and session. They are not interchangeable.
Do I need OIDC if I already use OAuth 2.0?
If you only need scoped API access, OAuth 2.0 is enough. The moment you need to log a user in and
know who they are, you need OpenID Connect — which you get by adding the openid scope and verifying
the ID token.
What is PKCE and why does it matter? PKCE (Proof Key for Code Exchange) binds the authorization request and the token exchange with a secret verifier, so an intercepted authorization code cannot be redeemed by an attacker. It is now recommended for all clients, including confidential web apps.
Conclusion
The confusion between OAuth 2.0 vs OpenID Connect disappears once you separate the two questions they answer: OAuth 2.0 decides what an app may do, OpenID Connect decides who the user is. Reach for OAuth when you are delegating API access, reach for OIDC when you are signing users in, and use Authorization Code with PKCE for both. Verify every ID token's signature, issuer, audience, expiry, and nonce, and you have an identity foundation strong enough for zero trust. From here, continue with why token storage matters, the broken access control incident, and the full security category.
References
- OAuth 2.0 Authorization Framework (RFC 6749) — the core authorization spec, roles, and grant types.
- OpenID Connect Core 1.0 — the identity layer, ID token, and claims defined on top of OAuth 2.0.
- PKCE (RFC 7636) — Proof Key for Code Exchange for authorization-code interception protection.
- OAuth 2.0 Security Best Current Practice — modern guidance, including why the implicit flow is discouraged.
- OpenID Connect FAQ — how OIDC relates to and extends OAuth 2.0.

