DevgainsDevgainsDevgains
All articles

OAuth 2.0 vs OpenID Connect: Authorization vs Authentication

·10 min read·Updated Jul 11, 2026

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 /userinfo endpoint.

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 + claims

The 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=S256

The 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_wW1gFWFOEjXk

The 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.0OpenID Connect (OIDC)
Primary purposeAuthorization (delegated access)Authentication (identity)
Question it answers"Can this app do X?""Who is this user?"
Core tokenAccess tokenID token (JWT) + access token
Token audienceThe API (resource server)The client (your app)
Token formatOpaque or JWT (undefined)Signed JWT with standard claims
Standard user infoNoneid_token claims + /userinfo
Identity guaranteesNoneiss, 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 openid scope and verify the ID token whenever you need login. Do not infer identity from an access token.
  • Validate iss, aud, exp, signature, and nonce on every ID token, using the provider's JWKS for key rotation.
  • Key users on sub, not email. Emails change and can be reassigned; sub is 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 state and nonce. state stops CSRF on the redirect; nonce stops 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=openid and 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, and nonce — 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

10 min read

Read next