02.06 · The authorization framework, not the auth one

OAuth 2.0

A delegated-authorization framework that almost every modern federation rides on. It is not "log in with Google" — that's OpenID Connect on top of OAuth. OAuth itself is the layer that lets one app act on your behalf at another.

Authentication vs authorization — the distinction OAuth makes you internalize

OAuth's most important contribution is forcing you to learn the difference between two questions that sound similar:

  • Authenticationwho are you? Establishes identity. OIDC, SAML, FIDO answer this.
  • Authorizationwhat are you allowed to do? Decides whether a specific action is permitted. OAuth 2.0 answers this.

OAuth was designed so that a third-party app (say, a calendar integration) could perform specific actions in another service (your Google Calendar) without the user handing over their Google password. Instead, the user grants the integration a scoped, revocable access token that lets it do narrow things ("read calendar events") on their behalf. The third-party never sees the password.

OpenID Connect later piggybacked on this to also answer the identity question, by adding an ID token to the response. That's why "Sign in with Google" works — it's OAuth carrying an OIDC payload.

The four roles in every OAuth flow

Role 1

Resource Owner

The user who owns the data. You, when "Acme Calendar" asks to access your Google Calendar.

Acme Calendar wants to read your Google Calendar — you're the owner.
Role 2

Client

The application asking for access. Has a registered client_id and (if confidential) a client_secret.

Acme Calendar (the third-party app).
Role 3

Authorization Server

The server that authenticates the resource owner and issues tokens. Operated by the platform whose data is being shared.

accounts.google.com (Google's auth server).
Role 4

Resource Server

The API that holds the protected data. Validates incoming access tokens and serves the data they're authorized for.

googleapis.com/calendar/v3 (the Calendar API).

In small implementations the authorization server and resource server are the same system. Conceptually they're separate; in production at any scale they actually are.

The grant types — pick the right tool

OAuth doesn't define one login flow; it defines several, optimized for different deployment shapes. Most of the historical attacks against OAuth come from using the wrong one.

GrantUsed byHow it worksStatus
Authorization CodeWeb apps with a backendBrowser gets a short-lived code via redirect, server exchanges it for tokens at the token endpoint using its client_secret.PREFERRED
Authorization Code + PKCESingle-page apps, mobile apps, anything without a server-side secretSame as auth code, but the client proves it started the flow with a one-time secret (the PKCE verifier). Defeats stolen-code attacks on public clients.PREFERRED for public clients
Implicit(Was: SPAs)Browser receives tokens directly in the redirect URL fragment. No backend involved.DEPRECATED 2020 — tokens leak via referrers, browser history, logs. Use Auth Code + PKCE.
Resource Owner Password(Was: legacy migrations)App collects username + password and sends them to the auth server. Defeats the whole point of OAuth.DEPRECATED 2020 — never use in new code.
Client CredentialsService-to-service (no user involved)Client authenticates itself with its client_id + client_secret. No user, no consent screen. For batch jobs, microservices.Valid for machine-to-machine
Device Authorization (Device Code)Smart TVs, CLIs, IoT — anywhere without a usable browserDevice shows a short code + URL. User opens that URL on their phone, types the code, approves. Device polls the token endpoint until approved.Valid for device flows
Refresh TokenLong-lived sessionsNot a grant by itself — a token type returned alongside the access token. Used to mint new access tokens without re-prompting the user.Valid; rotate refresh tokens for public clients

Which grant should you use?

Three quick questions narrow it down.

Pick a grant
1 / 3 · Is there a human user involved at all?
Pick an option above.

The Authorization Code flow — what an exchange actually looks like

The most important grant. Worth knowing the redirects by heart.

User clicks "Sign in with Google" on My-App
1
My-App redirects browser to https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=https://my-app.com/cb&scope=openid email&state=rand123&response_type=code&code_challenge=...&code_challenge_method=S256
2
Google checks client_id is real, redirect_uri matches the pre-registered allowlist, and prompts the user to log in (if not already signed in) and to consent ("Allow My-App to access your email?").
3
Google redirects browser back to https://my-app.com/cb?code=ABC123&state=rand123
4
My-App's server checks the state matches what it sent (CSRF defense), then makes a server-to-server POST to Google's token endpoint with code=ABC123, client_id, client_secret, code_verifier, grant_type=authorization_code.
5
Google verifies the code is fresh, the redirect_uri matches, the client is authentic (secret + PKCE verifier), and returns: access_token, id_token (a JWT — that's OIDC), refresh_token, expires_in.
6
My-App validates the ID token's signature against Google's published JWKS, extracts the user's identity, creates a local session. To call Google's APIs on the user's behalf it includes Authorization: Bearer <access_token> on every request until it expires; uses refresh_token to mint new ones.

Scopes — the "what" of authorization

Every access token is scoped: it permits certain operations and not others. The client requests a list of scopes in step 1; the user sees those scopes on the consent screen; the access token is issued with exactly that scope set (or a narrower one). The resource server checks the scope before serving each request.

Common scope examples:

  • openid — requests an ID token (OIDC).
  • email profile — basic identity claims.
  • https://www.googleapis.com/auth/calendar.readonly — read calendar events.
  • https://www.googleapis.com/auth/calendar.events — read AND write events.
  • repo — on GitHub, full repository access (overly broad; many apps request it when they only need repo:status).

Principle of least privilege applies: ask for the narrowest scope you can actually use. Apps that ask for everything teach users to consent without reading.

The pitfalls that keep producing CVEs

  • Loose redirect_uri allowlists. If the auth server accepts any redirect_uri matching https://my-app.com/*, an attacker who finds an open-redirect on my-app.com can steal the authorization code. Always use exact-match URI registration.
  • Missing state parameter. Without state validation, an attacker can trick a logged-in user into linking the attacker's account. The state acts like a CSRF token for the OAuth flow.
  • Implicit grant in SPAs. Tokens in URL fragments leak via browser history, server logs, Referer headers, and (historically) browser extensions. Auth Code + PKCE replaced this for a reason.
  • Confused-deputy via id_token. Some apps use an OIDC ID token as a bearer token to the wrong audience. ID tokens are for the client to verify identity; they are not API access tokens. Don't confuse the two.
  • Skipping aud claim validation on ID tokens. If the server doesn't check that aud is its own client_id, it'll accept tokens issued to different clients on the same IdP. Cross-app account hijack. The JWT page covers this in detail.
  • No refresh token rotation. A leaked refresh token gives perpetual access. Refresh tokens for public clients should be one-use; the auth server issues a new one with each refresh and revokes the previous.
  • Overly broad scopes by default. If your library requests every scope it might ever need, you've taught users to grant everything. Consent fatigue is a real attack surface.

Notable incidents

  • Pawn Storm OAuth phishing (2017, ongoing). Nation-state attackers built malicious OAuth apps named things like "Google Defender" and asked targets to grant them full mailbox scope. Once consented, the malicious app had legitimate API access — password rotations don't revoke OAuth grants.
  • Microsoft Teams "Storm-0558" (2023). Forged OIDC tokens used to access Outlook/M365. Root cause involved a leaked signing key; the OAuth/OIDC layer accepted them.
  • GitHub OAuth app phishing (recurring). Attackers make a benign-looking OAuth app, get developers to authorize it with repo scope, then exfiltrate every private repository the developer can see.
  • Slack and Discord token theft via malicious browser extensions. Bearer tokens stored client-side are stolen by extensions that can read the local storage of those sites. OAuth itself isn't broken — the storage of long-lived bearer tokens in environments where untrusted code runs is the problem.