OAuth 2.0 vs OpenID Connect: authorization is not authentication
OAuth 2.0 and OpenID Connect are used together so routinely that they are often treated as one thing, and the resulting confusion produces real vulnerabilities. The distinction is precise: OAuth 2.0 grants an application access to a resource; OpenID Connect tells an application who the user is. OIDC is a thin identity layer built on top of OAuth, and using OAuth alone to log people in is a well documented mistake with a name.
What OAuth 2.0 solves
OAuth exists to replace the password sharing anti pattern. Before it, letting a third party application read your calendar meant giving that application your password, with no way to limit its scope or revoke it short of changing the password everywhere.
OAuth introduces four parties and a delegation:
- The resource owner, the user.
- The client, the application asking for access.
- The authorization server, which authenticates the user and issues tokens.
- The resource server, the API holding the data.
The user authenticates directly with the authorization server, consents to a specific scope, and the client receives an access token: a bearer credential, scoped and time limited, that it presents to the API. The client never sees the password and cannot exceed the granted scope.
Critically, an access token is designed to be opaque to the client. It is addressed to the resource server, and its format is not part of the contract. It may be a JWT, or it may be a random string that only the authorization server can interpret. A client that parses it is depending on an implementation detail that can change without notice.
Why OAuth alone cannot log a user in
Consider a client that receives a valid access token and concludes "the user is signed in". What did it actually learn? That it holds a token granting some access. It has no standardized way to learn who the user is, when they authenticated, or whether this token was even issued to it.
That last point is the vulnerability, historically called the confused deputy problem. If a client accepts any access token and calls a /userinfo style endpoint to identify the user, then a malicious application can take a token that a user granted to it, present it to the honest client, and be logged in as that user. Nothing in the token binds it to the client that received it.
Providers papered over this for years with proprietary endpoints and non standard fields, which is why "sign in with X" implementations used to be subtly different for every X, and why several were broken.
What OpenID Connect adds
OIDC standardizes the identity layer on top of the same flows. Four additions:
The openid scope. Requesting it signals that you want identity, not just access.
The ID token. A JWT, always, returned alongside the access token. Unlike the access token, it is addressed to the client and is meant to be read by it:
{
"iss": "https://accounts.example.com",
"sub": "248289761001",
"aud": "your-client-id",
"exp": 1770000000,
"iat": 1769996400,
"nonce": "n-0S6_WzA2Mj",
"email": "user@example.com",
"email_verified": true
}
The claims that make it safe: iss names the issuer, aud names the client the token was issued to, nonce ties it to your specific authentication request, and sub is the stable, unique user identifier. Validating aud against your own client ID is precisely what closes the confused deputy hole. You can inspect the structure of any ID token with the JWT Decoder, which runs entirely in your browser; what is inside a JWT covers the anatomy.
Standard claims and a /userinfo endpoint, so profile fields mean the same thing across providers.
Discovery. A well known document at /.well-known/openid-configuration lists endpoints, supported flows, and the public keys (a JWKS URL) needed to verify signatures. This is what makes generic OIDC libraries possible.
| Access token | ID token | |
|---|---|---|
| Audience | The resource server | The client |
| Purpose | Authorize an API call | Identify the user |
| Format | Unspecified, often opaque | Always a JWT |
| Client should parse it | No | Yes, after validating |
| Sent to APIs | Yes | No |
The last row is worth stating plainly: do not send ID tokens to APIs as bearer credentials, and do not use access tokens to decide who the user is.
Validating an ID token
Signature verification alone is not enough. A complete check:
- Verify the signature against the issuer's JWKS, matching the
kidin the header. - Check
issexactly equals the expected issuer string. - Check
audcontains your client ID. - Check
exphas not passed andiatis sane. - Check
noncematches the value you generated for this request. - Reject
alg: noneand reject any algorithm you did not expect.
Skipping step 3 or step 5 is how the classic OIDC vulnerabilities happen. Skipping step 6 is how JWT algorithm confusion happens.
Which flow should you use?
The flow landscape has been simplified considerably by current best practice.
Authorization code with PKCE. The answer for essentially everything: web applications, single page applications, mobile apps, and desktop apps. The client redirects the user to the authorization server, receives a short lived code back, and exchanges that code for tokens over a direct back channel call. PKCE (proof key for code exchange) adds a per request secret: the client sends the hash of a random verifier up front and the verifier itself at exchange time, so a stolen code is useless without it. Generate the verifier with a secure random token generator and hash it with SHA-256 using the Hash Generator if you want to see the transformation.
PKCE was originally specified for mobile apps and is now recommended for confidential clients too, because it defends against code interception regardless of client type.
Client credentials. For machine to machine calls with no user involved. There is no OIDC equivalent, because there is no user to identify. Mutual TLS is a stronger alternative where you control both ends.
Deprecated and best avoided: the implicit flow, which returns tokens in the URL fragment where they land in browser history and referrer headers, and the resource owner password credentials grant, which reintroduces the password sharing OAuth was built to eliminate. Both are removed in the OAuth 2.1 consolidation.
Device authorization remains the right answer for televisions and CLI tools with no browser.
Where to keep the tokens
Once you have them, storage is its own decision with real tradeoffs between localStorage, memory, and cookies, and it is covered in where to store JWT tokens. The short version: for browser applications, an HttpOnly, Secure, SameSite=Lax cookie holding a session identifier is safer than any client readable storage, and the cookie flags matter as much as the token itself.
The summary
Use OAuth 2.0 when your application needs to call an API on the user's behalf. Use OpenID Connect when it needs to know who the user is. Almost every real login uses both at once, requesting the openid scope alongside whatever API scopes it needs, and treating the two resulting tokens as the different things they are.