OAuth 2.0: delegated authorization, not authentication
How OAuth 2.0 lets an app act on your behalf without your password — the four roles, access vs refresh tokens, the grant types and when to use each (Authorization Code + PKCE, Client Credentials, Device Code, Refresh Token; the deprecated Implicit and ROPC), scopes and consent, and what OAuth 2.1 changes. Plus the critical point: OAuth authorizes the client, it does not authenticate the user.
A different question than SAML asked
SAML answered “who is this user, and may they enter this app?” — authentication and SSO. OAuth answers a completely different question that exploded with the rise of APIs and third-party apps in the 2010s: how can an application act on your behalf against a service, without you handing it your password?
Picture it concretely. A photo-printing app wants to read Sara’s photos from her cloud storage. The terrible old way: Sara gives the printing app her cloud password, and now a random app has full control of her account forever. The OAuth way: Sara is redirected to her cloud provider, approves “let this app read your photos,” and the printing app receives a scoped, expiring token — never her password, never write access, never anything but the photos. That’s delegated authorization, and OAuth 2.0 (RFC 6749) is the framework for it.
The four roles
OAuth defines four parties, and untangling them is most of the battle:
- Resource Owner: the user who owns the data and grants access. Sara, in our example.
- Client: the application that wants access on the user’s behalf. The photo-printing app.
- Authorization Server (AS): the system that authenticates the resource owner, shows the consent screen, and issues tokens. Sara’s cloud provider’s identity service.
- Resource Server (RS): the API holding the protected data, which accepts access tokens. Sara’s cloud photo API.
flowchart LR accTitle: The four OAuth 2.0 roles and how they relate accDescr: The resource owner (user) grants consent at the authorization server. The authorization server authenticates the user and issues an access token to the client application. The client presents that access token to the resource server, which holds the protected API and returns the requested data. The client never receives the user's password. RO[Resource Owner<br/>the user] -->|approves access| AS[Authorization Server<br/>issues tokens] AS -->|access token| C[Client<br/>the app] C -->|presents token| RS[Resource Server<br/>the API] RS -->|protected data| C
The separation between Authorization Server and Resource Server is the design’s quiet genius. The AS is the only party that ever handles the user’s credentials and decisions; the RS just checks tokens. This is why one authorization server can guard hundreds of APIs, and why an API can accept tokens without knowing anything about how the user logged in.
Tokens: access and refresh
OAuth runs on bearer tokens — “bearer” meaning whoever holds the token can use it, like cash. There are two kinds, with very different lifecycles:
- Access token: the credential the client presents to the resource server on every API call. It’s scoped (it grants only specific permissions), short-lived (minutes to an hour), and used constantly — so it’s the more exposed of the two. If stolen, the damage is bounded by its scopes and its short life.
- Refresh token: a longer-lived credential the client stores privately and never sends to the resource server. When the access token expires, the client exchanges the refresh token at the authorization server for a fresh access token — so the user doesn’t have to re-consent every few minutes. It’s used rarely and guarded carefully; its theft is far more serious, which is why OAuth 2.1 requires rotating or sender-constraining it.
How the resource server validates a token
When an access token arrives, the resource server must decide if it’s genuine and what it permits. Two models dominate, and knowing which you’re using matters:
- Self-contained (JWT) tokens: the access token is a signed JWT the RS validates locally — check the signature against the AS’s public key, then verify the issuer (
iss), audience (aud), expiry (exp), and scopes. Fast and network-free, but revocation is weak: the token stays valid until it expires. - Reference tokens + introspection: the access token is an opaque string the RS sends to the AS’s introspection endpoint (RFC 7662), which answers whether it’s active and what scopes it carries. A network call per check, but it supports instant revocation.
In both models the audience check is non-negotiable: the RS must confirm the token was minted for it, not for some other API. Skip it and a token leaked from one low-value service can be replayed against a high-value one — a common real-world failure. (We dissect JWT structure in the Tokens & Formats article.)
Grant types: the flows and when to use each
A grant type (or flow) is the procedure a client uses to obtain a token. Choosing the right one for your client type is the core OAuth design decision.
Authorization Code + PKCE — the default for user-facing apps
This is the flow you should reach for in web apps, single-page apps, and mobile apps. The user authenticates at the authorization server, the client receives a short-lived authorization code via redirect, and then exchanges that code for tokens on a separate request — protected by PKCE.
sequenceDiagram accTitle: OAuth 2.0 Authorization Code flow with PKCE accDescr: The client generates a PKCE code verifier and its hashed challenge. It redirects the user to the authorization server with the code challenge. The authorization server authenticates the user and shows a consent screen, then redirects back to the client with a one-time authorization code. The client exchanges the code plus the original verifier at the token endpoint, and the authorization server validates the verifier against the earlier challenge and returns an access token and refresh token. actor U as User participant C as Client (app) participant AS as Authorization Server participant RS as Resource Server C->>C: Generate PKCE verifier + challenge C->>AS: Authorize request (+ code_challenge) AS->>U: Authenticate + consent AS->>C: Authorization code (one-time) C->>AS: Exchange code + code_verifier AS->>AS: Verify challenge matches verifier AS->>C: Access token (+ refresh token) C->>RS: API call with access token RS->>C: Protected data
PKCE (Proof Key for Code Exchange, RFC 7636) is the safeguard that makes this flow robust. The client invents a random secret (the verifier), hashes it into a challenge, and sends only the challenge when starting the flow. When it redeems the authorization code, it must present the original verifier — and the AS checks that it hashes to the challenge it saw earlier. The effect: even if an attacker intercepts the authorization code (on a mobile device, in browser history, in logs), they can’t exchange it for tokens without the secret they never saw. PKCE began as a mobile fix and is now recommended — and in OAuth 2.1 required — for every Authorization Code flow.
Client Credentials — machine to machine
When there’s no user — a backend service calling another service’s API, a cron job, a microservice — there’s no one to redirect or consent. The client authenticates as itself with its own credentials and receives an access token scoped to what that service may do. This is the right flow for machine-to-machine integration, and it’s the one place there’s no resource owner at all (the client is, in effect, acting for itself).
sequenceDiagram accTitle: OAuth 2.0 Client Credentials flow accDescr: A backend service authenticates directly to the authorization server using its own client ID and client secret, optionally requesting scopes. The authorization server returns an access token scoped to what the service may do. The service then calls the resource server API with that token and receives the protected data. No user is involved at any point. participant SVC as Calling service (client) participant AS as Authorization Server participant RS as Resource Server (API) SVC->>AS: client_id + client_secret (+ scope) AS->>SVC: Access token SVC->>RS: API call with access token RS->>SVC: Protected data
Device Code — for things with no keyboard
Smart TVs, streaming sticks, CLI tools, IoT devices: typing a password on them is painful or impossible. The Device Authorization Grant (RFC 8628) solves this elegantly — the device shows a short code and a URL, the user opens that URL on their phone or laptop, enters the code, and approves there, while the device polls the AS until the approval lands. The constrained device never handles credentials; the user authenticates on a capable device.
sequenceDiagram
accTitle: OAuth 2.0 Device Authorization Grant (Device Code flow)
accDescr: An input-constrained device requests a device code and user code from the authorization server, which returns a device code, a user code, and a verification URL. The device displays the user code and URL. The user opens that URL on a separate phone or laptop, signs in, and approves. Meanwhile the device polls the token endpoint repeatedly until the authorization server reports approval and returns an access token.
participant DEV as Device (TV / IoT)
participant AS as Authorization Server
actor U as User (phone / laptop)
DEV->>AS: Request device + user code
AS->>DEV: device_code, user_code, verification_uri
DEV->>U: Display "go to URL, enter CODE"
U->>AS: Open URL, sign in, approve
loop Until approved
DEV->>AS: Poll token endpoint (device_code)
end
AS->>DEV: Access tokenRefresh Token — renewing without re-consent
Not a “login” flow but a renewal mechanism: the client exchanges its refresh token for a new access token when the old one expires, keeping the session alive without bothering the user. Because refresh tokens are powerful and long-lived, modern practice is refresh token rotation — each use issues a new refresh token and invalidates the old one, so a stolen-and-replayed refresh token is detectable.
Implicit and ROPC — deprecated, do not use
Two early grants are now actively discouraged and removed in OAuth 2.1:
- Implicit grant: returned the access token directly in the browser redirect (in the URL fragment), with no code exchange. It leaked tokens into browser history, logs, and referrers. Authorization Code + PKCE replaces it even for single-page apps.
- Resource Owner Password Credentials (ROPC): the client collects the user’s actual username and password and sends them to the AS. This defeats the entire point of OAuth — the client sees the password — and breaks MFA and federation. Never use it for new work.
| Grant type | Use it for | Status |
|---|---|---|
| Authorization Code + PKCE | Web, SPA, and mobile apps (a user is present) | Preferred |
| Client Credentials | Machine-to-machine, no user | Current |
| Device Code | TVs, IoT, CLIs — input-constrained devices | Current |
| Refresh Token | Renewing access without re-consent | Current |
| Implicit | (was: SPAs) | Deprecated — use Code + PKCE |
| ROPC | (was: legacy/first-party) | Deprecated — never for new work |
Scopes and consent
Scopes are how OAuth expresses least privilege for delegated access. A scope is a string naming a permission — photos.read, calendar.write, profile — and the client requests only the scopes it needs. The access token is minted carrying those scopes, and the resource server enforces them: a token with photos.read can’t write.
Consent is where the resource owner sees and approves what’s being requested — the “this app wants to: read your photos” screen. It’s the moment the user, not the platform, decides. Two best practices define good scope and consent design:
- Request narrowly. Ask for the least scope that does the job. A printing app that requests full account access when it needs only photo-read is a red flag, and increasingly platforms and users reject it.
- Make consent honest. The consent screen should describe real capabilities in plain language. Over-broad scopes hidden behind vague consent are how users get tricked into over-granting — the delegated-authorization version of privilege creep.
Two refinements you’ll meet in mature deployments sharpen this further:
- Incremental authorization. Rather than demanding every scope up front, an app requests the minimum to get started and asks for more only when a feature actually needs them. The first consent screen stays small and honest, users aren’t scared off by a wall of permissions, and the app holds broad scope only when it’s genuinely in use.
- Resource indicators (RFC 8707). A client can name which resource server a token is for, so the authorization server mints a token narrowly scoped to that one API instead of a broad token valid everywhere. This shrinks the blast radius of a leaked token and reinforces the audience check the resource server performs.
OAuth 2.1: making the safe path the only path
OAuth 2.1 is not a new protocol — it’s a consolidation that folds ten years of security guidance (much of it from the OAuth Security Best Current Practice) into a single, cleaner baseline. The headline changes:
- PKCE is mandatory for all Authorization Code flows, public and confidential clients alike.
- The Implicit grant is removed.
- The Resource Owner Password Credentials grant is removed.
- Redirect URIs must be compared with exact string matching — no wildcards, closing a long-abused open-redirect vector.
- Refresh tokens must be sender-constrained or rotated.
The through-line is deletion, not addition: OAuth 2.0 offered insecure options that developers kept choosing, so 2.1 simply removes them. If you learn OAuth 2.1’s defaults, you’ve learned the consensus on how to do OAuth safely.
Security essentials
OAuth’s flexibility is also its footgun; the OAuth 2.0 Security Best Current Practice catalogues the hazards. The essentials:
- Validate redirect URIs exactly. Loose matching lets an attacker redirect the code or token to themselves — historically the most damaging OAuth flaw.
- Always use PKCE. It neutralizes authorization-code interception.
- Use the
stateparameter. It binds the request to the user’s session and blocks CSRF on the redirect. - Treat tokens as secrets. Access tokens are bearer credentials — over TLS only, never in URLs or logs, stored carefully on the client.
- Rotate refresh tokens and keep access-token lifetimes short, so theft has a small blast radius.
- Audience-restrict tokens. Mint each token for a specific resource server and have every resource server reject tokens not addressed to it, so a token can’t hop from a low-value API to a high-value one.
The trap that created OpenID Connect
Because OAuth got an app a token “after the user logged in at the provider,” developers assumed a valid access token meant the user was authenticated — and built logins on it. The problem: an access token is bearer permission, not proof of who or even whether a user is present. A token minted for one app could be replayed at another; a token could belong to a machine. Using OAuth as authentication produced real account-takeover bugs.
The fix wasn’t to abandon OAuth but to add a proper identity layer on top of it — a standard token that does assert who the user is, who it’s for, and when they authenticated. That layer is OpenID Connect, and it’s the next article. Keep the distinction crisp: OAuth authorizes the client; OIDC authenticates the user. Same redirects, same token machinery, one added assertion of identity — but a world of difference in what you may safely conclude from it.
Recap
OAuth 2.0 is delegated authorization, not authentication:
- Four roles: resource owner (user), client (app), authorization server (issues tokens), resource server (the API).
- Two tokens: short-lived, scoped access tokens used on every call, and guarded, long-lived refresh tokens exchanged for new access tokens.
- Grant types fit client types: Authorization Code + PKCE (user-facing apps), Client Credentials (machine-to-machine), Device Code (no keyboard), Refresh Token (renewal) — and never Implicit or ROPC.
- Scopes + consent deliver least privilege: request narrowly, consent honestly.
- OAuth 2.1 makes the safe path the only path: PKCE mandatory, insecure grants removed, exact redirect matching.
- OAuth is not authentication — an access token is permission, not identity, which is exactly why OIDC exists.
Hands-on exercises
These are practical — do them, don’t just read them:
- Watch a real flow. Open your browser’s network tab and use a “Sign in with Google/GitHub” button on any site. Find the
authorizerequest and identify theclient_id,scope,redirect_uri,state, andcode_challengeparameters — then find where the authorization code comes back. - Decode the consent. On a provider’s app-permissions page (e.g., your Google or GitHub “third-party access” settings), pick one app and list the exact scopes it holds. Decide which are over-broad for what the app does.
- Pick the grant (use case). For each, name the correct grant type and justify it: a smart-TV streaming app; a nightly billing job calling a payments API; a React single-page app; a mobile banking app.
- Break it on paper (use case). An SPA uses the Implicit grant and matches redirect URIs by prefix. Describe two concrete attacks this enables and the exact OAuth 2.1 changes that close them.
- Design M2M access (use case). Two of your backend services need to talk. Specify the grant type, how the calling service authenticates, the scopes you’d define, and how you’d rotate its credentials.
- Inspect a token (use case). Obtain an access token from a provider’s developer playground (e.g., Google’s OAuth Playground). If it’s a JWT, decode it and find
aud,scope, andexp; if it’s opaque, describe how the resource server would validate it instead. Then state what the resource server must check before trusting it.
Three questions to test yourself
- A developer says “we use OAuth to log users in — if we get a valid access token, we know who they are.” Explain precisely why this is wrong and what they should use instead.
- Why is a refresh token treated as far more sensitive than an access token, and what two practices limit the damage if one is stolen?
- Walk through how PKCE defeats authorization-code interception, step by step, and explain why a stolen authorization code alone is useless to an attacker.
Frequently asked questions
What is OAuth 2.0?
OAuth 2.0 (RFC 6749) is a delegated authorization framework: it lets an application obtain limited access to a user's resources on another service — read their calendar, post to their feed — without ever seeing the user's password. The user approves a scoped, time-limited access token that the app presents to the API. It's about granting an app permission to act, not about logging a user in.
Does OAuth 2.0 authenticate the user?
No — and this is the most important and most misunderstood point. OAuth authorizes a client to access resources; it says nothing reliable about who the user is to that client. An access token is proof of permission, not proof of identity. Apps that try to use a raw OAuth access token as a login are committing a well-known anti-pattern; authenticating the user is exactly what OpenID Connect adds on top of OAuth.
What is the difference between an access token and a refresh token?
An access token is a short-lived credential the client presents to the resource server to call an API on the user's behalf; it carries scopes and typically expires in minutes. A refresh token is a longer-lived credential the client keeps privately and exchanges at the authorization server for new access tokens when they expire, so the user doesn't have to re-approve constantly. Access tokens are used often and exposed; refresh tokens are guarded.
Which OAuth 2.0 grant type should I use?
Authorization Code with PKCE for almost everything user-facing — web apps, single-page apps, and mobile apps. Client Credentials for machine-to-machine calls with no user involved. Device Code for input-constrained devices like TVs and IoT. Refresh Token to renew sessions. Avoid the Implicit and Resource Owner Password Credentials grants entirely — they're deprecated in OAuth 2.1.
What is PKCE and why is it required?
PKCE (Proof Key for Code Exchange, RFC 7636) protects the Authorization Code flow from interception. The client generates a secret 'verifier', sends only its hash on the way out, and reveals the verifier when redeeming the code — so an attacker who steals the authorization code can't exchange it without the secret. Originally for mobile apps, PKCE is mandatory for all Authorization Code flows in OAuth 2.1.
What changed in OAuth 2.1?
OAuth 2.1 consolidates a decade of best practice into one baseline: PKCE is mandatory for all Authorization Code flows, the Implicit and Resource Owner Password Credentials grants are removed, redirect URIs must match exactly, and refresh tokens must be sender-constrained or rotated. It doesn't invent new protocols — it removes the insecure options people kept choosing and makes the safe path the only path.