OAuth is not authentication - OIDC is
The distinction that causes the most real security bugs.
Open this lesson in the learning hubKey points
- OAuth 2 is an authorisation framework. It answers "may this application access that resource", and says nothing about who the user is.
- An access token is not proof of identity. It is a bearer credential for an API, and it may not identify a user at all - client credentials tokens identify a machine.
- OpenID Connect is a thin layer on top that adds authentication: an
id_token, a standard/userinfoendpoint, and theopenidscope that requests them. - The
id_tokenis a JWT for your client, withaudset to your client id. It is the thing you validate to learn who signed in. - The classic vulnerability is using an access token to identify a user - calling a provider profile endpoint with a token that came from somewhere else, and trusting the answer.
- That is the confused deputy problem: a token obtained by a malicious app for its own use is presented to yours, and without checking
audyou accept it as proof of that user identity.
Example
# OAuth 2 alone - authorisation only
#
# scope=orders.read
# -> access_token "the bearer may read orders"
# -> that is ALL it says
# OIDC - adds authentication
#
# scope=openid profile email
# -> access_token for the API
# -> id_token WHO signed in, and for WHICH client
---
# The id_token, decoded:
{
"iss": "https://accounts.google.com",
"aud": "my-client-id.apps.googleusercontent.com", # <- YOUR client id
"sub": "110169484474386276334", # stable user id
"email": "user@example.com",
"email_verified": true,
"nonce": "abc789", # from your request
"exp": 1754251200
}
# VALIDATE, in this order:
# 1. signature, against the provider JWKS
# 2. iss == the expected issuer
# 3. aud == YOUR client id <- this is the confused-deputy check
# 4. exp not expired
# 5. nonce == what you sent
# 6. email_verified is true, if you use email at all
---
# THE VULNERABILITY:
#
# 1. attacker builds an app, user grants it access
# 2. attacker now holds a valid access token for that user
# 3. attacker sends that token to YOUR app "sign me in with this"
# 4. your app calls the provider profile endpoint with it, gets the
# victim details, and logs the attacker in as the victim
#
# The id_token aud check is what makes this impossible: a token minted
# for the attacker client will never carry YOUR client id.
An access token says what may be accessed, never who is asking - authenticate with an id_token and check its audience.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OAuth 2.0 Course course, and every lesson in it is listed on the OAuth 2.0 Course contents page.