OAuth 2.0: Roles and Vocabulary

OAuth is a delegation protocol, not a login protocol. Getting that straight makes everything else follow.

OAuth 2.0 answers one question: how can an application act on a user's behalf against an API, without the user handing over their password?

Before OAuth, integrations asked for your actual credentials — you gave a third-party site your email password so it could read your contacts. That gave it everything, forever, with no way to take it back except changing the password. OAuth replaces that with a scoped, revocable, expiring token.

The four roles

RoleWhoExample
Resource ownerthe userAlice
Clientthe app wanting accessa calendar app
Authorization serverissues tokensaccounts.google.com
Resource serverholds the dataGoogle Calendar API

The client never sees Alice's password. It sends her to the authorization server, she authenticates there, approves a specific scope, and the client receives a token limited to exactly that.

Confidential vs public clients

  • Confidential — runs on a server and can keep a client secret. Web backends.
  • Public — runs where the user can read its code: SPAs, mobile apps, desktop apps. It cannot hold a secret, no matter how well obfuscated. Public clients must use PKCE.

The endpoints

  • /authorize — where you send the user's browser. Interactive.
  • /token — where the client exchanges a code for tokens. Server-to-server.
  • /introspect, /revoke, /userinfo, /.well-known/openid-configuration — supporting endpoints.

The mistake everyone makes

OAuth 2.0 is not authentication. An access token says "the bearer may read Alice's calendar". It does not say "this is Alice", and it does not say to whom it was issued. Building login on a raw access token leads to real vulnerabilities — a token obtained by a different app can be replayed at your login endpoint.

The layer that does do authentication is OpenID Connect, which adds an ID token with an audience naming your client. If you want "Sign in with Google", you want OIDC.

Example

Example · bash
# The whole point, in one picture

#  BEFORE OAuth
#    Alice → gives her Google password → to a calendar app
#    the app can now do ANYTHING as Alice, forever

#  WITH OAuth
#    Alice → authenticates at accounts.google.com (the app never sees this)
#          → approves: "Calendar app wants to read your calendar"
#    the app receives: a token for calendar.readonly, expiring in 1 hour,
#                      revocable by Alice at any time

# Discovery: every compliant provider publishes its endpoints
curl https://accounts.google.com/.well-known/openid-configuration | jq '{
  authorization_endpoint, token_endpoint, jwks_uri, userinfo_endpoint
}'

When to use it

  • A scheduling app reads a user's Google Calendar with a read-only scope, and the user can revoke it from their Google account page without changing any password.
  • A CI provider is granted repo:status on GitHub instead of full account access, so a breach at the provider cannot delete repositories.
  • A company's internal apps all delegate login to one authorization server, so offboarding an employee in one place removes access everywhere.

More examples

Reading a provider's discovery document

Fetching and caching this document is better than hardcoding endpoints: providers do move them, and the jwks_uri in particular must be read dynamically for key rotation.

Example · bash
# Every OIDC-compliant provider exposes this. Read it before writing any code —
# it tells you the endpoints, the algorithms, and the flows actually supported.
curl -s https://accounts.google.com/.well-known/openid-configuration | jq

{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint": "https://oauth2.googleapis.com/token",
  "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
  "revocation_endpoint": "https://oauth2.googleapis.com/revoke",
  "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
  "response_types_supported": ["code", "token", "id_token", ...],
  "id_token_signing_alg_values_supported": ["RS256"],
  "code_challenge_methods_supported": ["plain", "S256"],
  "scopes_supported": ["openid", "email", "profile"]
}

# code_challenge_methods_supported containing S256 = PKCE is available. Use it.

Confidential and public clients, side by side

This distinction determines which flow you may use. Registering an SPA as a confidential client to avoid PKCE is a misconfiguration, not a shortcut.

Example · javascript
// CONFIDENTIAL — a server-side web app. It can keep a secret.
const confidential = {
  clientId: 'abc-web',
  clientSecret: process.env.OAUTH_CLIENT_SECRET,   // never leaves the server
  redirectUri: 'https://abc.com/auth/callback',
  // Authenticates itself at /token with the secret.
};

// PUBLIC — an SPA or a mobile app. Anything it holds, the user can read.
const publicClient = {
  clientId: 'abc-spa',
  // NO secret. Not obfuscated, not in an env var baked into the bundle,
  // not "hidden" in a native binary — all of those are readable.
  redirectUri: 'https://abc.com/auth/callback',
  usePKCE: true,                 // ← this is what replaces the secret
};

// A "secret" shipped to a browser or a phone is a published secret. Treat any
// client the user can inspect as public, and use PKCE.

Discussion

  • Be the first to comment on this lesson.