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
| Role | Who | Example |
|---|---|---|
| Resource owner | the user | Alice |
| Client | the app wanting access | a calendar app |
| Authorization server | issues tokens | accounts.google.com |
| Resource server | holds the data | Google 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
# 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.
# 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.
// 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