OAuth is not authentication - OIDC is

OAuth 2.0 Course · lesson 11 of 13 · 6 min read

The distinction that causes the most real security bugs.

Open this lesson in the learning hub

Key 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 /userinfo endpoint, and the openid scope that requests them.
  • The id_token is a JWT for your client, with aud set 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 aud you 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.