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 and refresh tokens, the grant types and when to use each (Authorization Code with PKCE, Client Credentials, Device Code, Refresh Token, and the deprecated Implicit and password grants), scopes and consent, and what OAuth 2.1 changes. Plus the key point: OAuth authorizes the client; it doesn't authenticate the user.

A different question from SAML’s

Sara wants to print a few photos. The printing app she found asks to connect to her cloud storage. Ten years earlier, the way to do that would have been to type her cloud password into the printing app, which would then have full control of her account for as long as it liked. Today she’s sent to her cloud provider’s own page, which asks whether she wants to let this app read her photos. She says yes, comes back, and the app can see her photos. It never saw her password, it can’t delete anything, and the access will run out on its own.


That’s delegated authorization, and OAuth 2.0 (RFC 6749) is the framework for it. SAML answered “who is this user, and may they enter this app?”, which is authentication and single sign-on. OAuth answers a different question, one that took off with 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?



The four roles

OAuth has four parties, and sorting them out is most of the work.


The resource owner is the user who owns the data and grants access: Sara. The client is the application that wants access on her behalf: the printing app. The authorization server authenticates the resource owner, shows the consent screen, and issues tokens: her cloud provider’s identity service. And the resource server is the API holding the protected data, which accepts access tokens: her cloud provider’s photo API.


flowchart LR
  accTitle: The four OAuth 2.0 roles and how they relate
  accDescr: The resource owner, the user, approves access at the authorization server. The authorization server authenticates the user and issues an access token to the client application. The client presents that token to the resource server, which holds the protected API and returns the 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 four roles: the resource owner approves access at the authorization server, which issues a token to the client, which presents it to the resource server. The client never sees the user's password.

Separating the authorization server from the resource server is the quiet good idea at the center of the design. The authorization server is the only party that ever handles the user’s credentials and decisions; the resource server just checks tokens. That’s why one authorization server can protect hundreds of APIs, and why an API can accept tokens without knowing anything about how the user signed in.


Access tokens and refresh tokens

OAuth runs on bearer tokens: whoever holds one can use it, like cash. There are two kinds, with very different lives.


An access token is what the client presents to the resource server on every API call. It’s scoped, granting only specific permissions; it’s short-lived, minutes to an hour; and it’s used constantly, which makes it the more exposed of the two. If it’s stolen, the damage is limited by its scopes and its short life.


A refresh token lasts longer, stays private to the client, and is never sent to the resource server. When the access token expires, the client exchanges the refresh token at the authorization server for a new access token, so the user doesn’t have to approve again every few minutes. It’s used rarely and guarded carefully, because stealing one is far more serious, which is why OAuth 2.1 requires refresh tokens to be rotated or tied to the client that holds them.


A resource server that receives a valid access token knows that whoever holds it was granted these scopes. It doesn’t reliably know who the user is; the token may even belong to a machine with no user at all. The token answers what may be done, never who is doing it.


How the resource server checks a token

When an access token arrives, the resource server has to decide whether it’s genuine and what it allows. There are two common models, and it matters which one you’re using.


With self-contained tokens, the access token is a signed JWT that the resource server checks locally: it verifies the signature against the authorization server’s public key, then checks the issuer (iss), the audience (aud), the expiry (exp), and the scopes. It’s fast and needs no network call, but revocation is weak, because the token stays valid until it expires.


With reference tokens, the access token is an opaque string that the resource server sends to the authorization server’s introspection endpoint (RFC 7662), which answers whether it’s active and what scopes it carries. That costs a network call per check, but revocation is immediate.


In both models the audience check is non-negotiable. The resource server must confirm the token was issued for it and not for some other API. Skip it, and a token leaked from a low-value service can be replayed against a high-value one, a common real-world failure. The tokens and formats article takes JWTs apart in detail.


Grant types: which flow for which client

A grant type, or flow, is the procedure a client uses to get a token. Picking the right one for the kind of client you’re building is the central OAuth design decision.


Authorization Code with PKCE

This is the flow for web apps, single-page apps, and mobile apps, anywhere a person is present. The user authenticates at the authorization server, the client receives a short-lived authorization code through a redirect, and then exchanges that code for tokens in a separate request, protected by PKCE. It’s what happened when Sara connected the printing app.


sequenceDiagram
  accTitle: OAuth 2.0 Authorization Code flow with PKCE
  accDescr: The client creates a PKCE code verifier and its hashed challenge. It sends 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 checks the verifier against the earlier challenge and returns an access token and a refresh token.
  actor U as User
  participant C as Client (app)
  participant AS as Authorization server
  participant RS as Resource server
  C->>C: Create PKCE verifier and challenge
  C->>AS: Authorize request (+ code_challenge)
  AS->>U: Authenticate and consent
  AS->>C: Authorization code (one-time)
  C->>AS: Exchange code + code_verifier
  AS->>AS: Check challenge matches verifier
  AS->>C: Access token (+ refresh token)
  C->>RS: API call with access token
  RS->>C: Protected data
Authorization Code with PKCE: the client gets a one-time code through the browser, then exchanges it for tokens with a secret only it knows, so a stolen code is useless to an attacker.

PKCE, Proof Key for Code Exchange (RFC 7636), is what makes the flow robust. The client makes up a random secret, the verifier, hashes it into a challenge, and sends only the challenge when the flow starts. When it redeems the authorization code, it has to present the original verifier, and the authorization server checks that it hashes to the challenge it saw earlier. So even if an attacker intercepts the authorization code, on a phone, in browser history, in a log, they can’t exchange it for tokens without a secret they never saw. PKCE started as a fix for mobile apps; it’s now recommended for every Authorization Code flow, and OAuth 2.1 requires it.


Client Credentials

When there’s no user, a back-end service calling another service’s API, a scheduled job, a microservice, there’s nobody to redirect or ask for consent. The client authenticates as itself, with its own credentials, and gets an access token limited to what that service may do. It’s the right flow for machine-to-machine integration, and the one case with no resource owner at all: the client is, in effect, acting for itself.


sequenceDiagram
  accTitle: OAuth 2.0 Client Credentials flow
  accDescr: A back-end service authenticates directly to the authorization server with its own client ID and client secret, optionally asking for scopes. The authorization server returns an access token scoped to what the service may do. The service calls the resource server's 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
Client Credentials: no user, no redirect, no consent. The service authenticates as itself and gets a scoped token.

Device Code

Smart TVs, streaming sticks, command-line tools, IoT devices: typing a password on them is painful or impossible. The Device Authorization Grant (RFC 8628) handles this neatly. The device shows a short code and a URL, the user opens the URL on a phone or laptop, enters the code, and approves there, while the device keeps asking the authorization server until the approval arrives. The device never handles credentials; the user authenticates on something with a keyboard.


sequenceDiagram
  accTitle: OAuth 2.0 Device Authorization Grant
  accDescr: A device with no keyboard asks the authorization server for a device code and a user code, and receives a device code, a user code, and a verification URL. The device shows the user code and the URL. The user opens the URL on a phone or laptop, signs in, and approves. Meanwhile the device polls the token endpoint 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 and user code
  AS->>DEV: device_code, user_code, verification_uri
  DEV->>U: Show "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 token
Device Code: the device shows a code and keeps polling while the user approves on a real keyboard somewhere else. Authentication happens on the capable device, never on the TV.

Refresh Token

This isn’t a sign-in flow but a renewal mechanism: the client exchanges its refresh token for a new access token when the old one expires, keeping access alive without bothering the user. Because refresh tokens are powerful and long-lived, current practice is rotation: every use issues a new refresh token and invalidates the old one, so a stolen token that gets replayed shows up as an anomaly.


Implicit and the password grant: don’t use them

Two early grants are now actively discouraged, and OAuth 2.1 removes them. The Implicit grant returned the access token directly in the browser redirect, in the URL fragment, with no code exchange, and leaked tokens into browser history, logs, and referrer headers; Authorization Code with PKCE replaces it, even for single-page apps. The Resource Owner Password Credentials grant had the client collect the user’s actual username and password and send them to the authorization server, which defeats the entire point of OAuth, since the client sees the password, and breaks MFA and federation. Neither belongs in anything new.


Grant typeUse it forStatus
Authorization Code + PKCEWeb, single-page, and mobile apps, where a user is presentPreferred
Client CredentialsMachine to machine, no userCurrent
Device CodeTVs, IoT, command-line tools, devices with no keyboardCurrent
Refresh TokenRenewing access without asking againCurrent
ImplicitFormerly single-page appsDeprecated; use Code + PKCE
Password (ROPC)Formerly legacy first-party appsDeprecated; never for new work

A rule of thumb covers most real decisions. A person using a browser or an app means Authorization Code with PKCE. Service to service with nobody present means Client Credentials. No keyboard means Device Code. Staying signed in means a rotated refresh token. And anyone proposing Implicit or the password grant for new work should be stopped.


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 asks only for the scopes it needs. The access token carries those scopes, and the resource server enforces them: a token with photos.read can’t write.


Consent is the moment the resource owner sees what’s being requested and approves it, the “this app wants to read your photos” screen. It’s where the user, not the platform, decides. Two habits define good scope and consent design. Ask narrowly: request the least scope that does the job. A printing app asking for full account access when it only needs to read photos is a warning sign, and users and platforms increasingly refuse it. And make consent honest: describe real capabilities in plain language. Broad scopes hidden behind vague consent are how users are tricked into granting too much, the delegated version of privilege creep.


Mature deployments add two refinements. With incremental authorization, an app asks for the minimum to get started and requests more only when a feature actually needs it, so the first consent screen stays small and honest and the app holds broad scope only while it’s in use. With resource indicators (RFC 8707), a client names which resource server a token is for, and the authorization server issues a token narrowly limited to that API instead of one valid everywhere. That reduces what a leaked token can do and reinforces the audience check on the resource server.


OAuth 2.1: making the safe path the only path

OAuth 2.1 isn’t a new protocol. It gathers ten years of security guidance, much of it from the OAuth Security Best Current Practice, into one cleaner baseline. PKCE becomes mandatory for every Authorization Code flow, for public and confidential clients alike. The Implicit and password grants are removed. Redirect URIs must be compared by exact string match, with no wildcards, closing a long-abused open-redirect hole. And refresh tokens must be sender-constrained or rotated.


The common thread is deletion rather than addition. OAuth 2.0 offered insecure options that developers kept choosing, so 2.1 takes them away. Learning OAuth 2.1’s defaults is learning the consensus on how to do OAuth safely.


Security essentials

OAuth’s flexibility is also how people hurt themselves with it, and the OAuth 2.0 Security Best Current Practice catalogs the hazards. The essentials fit in a paragraph each.


Validate redirect URIs exactly. Loose matching lets an attacker send the code or token to themselves, historically the most damaging OAuth flaw. Always use PKCE, which neutralizes intercepted authorization codes. Use the state parameter, which ties the request to the user’s session and blocks cross-site request forgery on the redirect.


Treat tokens as secrets. Access tokens are bearer credentials: send them over TLS only, never put them in URLs or logs, and store them carefully on the client. Rotate refresh tokens and keep access tokens short-lived, so a theft does limited damage. And restrict each token to an audience: issue it for a specific resource server and have every resource server reject tokens not addressed to it, so a token can’t jump from a low-value API to a high-value one.


The trap that created OpenID Connect

Because OAuth handed an app a token after the user signed in at the provider, developers assumed a valid access token meant the user was authenticated, and built sign-ins on top of it. But an access token is bearer permission, not proof of who the user is, or even that a user is present. A token issued 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: a standard token that does say who the user is, which app it’s for, and when they authenticated. That layer is OpenID Connect, the next article. Keep the distinction sharp: OAuth authorizes the client; OIDC authenticates the user. Same redirects, same token machinery, one added statement of identity, and a world of difference in what you can safely conclude.


Recap

OAuth 2.0 is delegated authorization, not authentication.


  1. There are four roles: the resource owner (the user), the client (the app), the authorization server (which issues tokens), and the resource server (the API).
  2. There are two tokens: short-lived, scoped access tokens used on every call, and longer-lived, guarded refresh tokens exchanged for new access tokens.
  3. Grant types fit kinds of client: Authorization Code with PKCE for apps with a user, Client Credentials for machine to machine, Device Code for devices without a keyboard, Refresh Token for renewal, and never Implicit or the password grant.
  4. Scopes and consent deliver least privilege: ask narrowly and describe access honestly.
  5. OAuth 2.1 makes the safe path the only one: PKCE mandatory, insecure grants removed, exact redirect matching.
  6. OAuth isn’t authentication. An access token is permission, not identity, which is exactly why OIDC exists.


Hands-on exercises

These are meant to be done, not just read.


  1. Watch a real flow. Open your browser’s network tab and use a “Sign in with Google” or “Sign in with GitHub” button on any site. Find the authorize request, identify the client_id, scope, redirect_uri, state, and code_challenge parameters, and then find where the authorization code comes back.
  2. Read the consent. On a provider’s app permissions page, such as your Google or GitHub third-party access settings, pick one app and list the exact scopes it holds. Decide which are broader than what the app does.
  3. Pick the grant. Name the right grant type for each, with a reason: a smart TV streaming app, a nightly billing job calling a payments API, a React single-page app, and a mobile banking app.
  4. Break it on paper. A single-page app uses the Implicit grant and matches redirect URIs by prefix. Describe two concrete attacks this allows and the OAuth 2.1 changes that close them.
  5. Design machine-to-machine access. Two of your back-end 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.
  6. Inspect a token. Get an access token from a provider’s developer playground, such as Google’s OAuth Playground. If it’s a JWT, decode it and find aud, scope, and exp; if it’s opaque, describe how the resource server would check it instead. Then list what the resource server must verify before trusting it.

Three questions to test yourself

  1. A developer says, “We use OAuth to sign users in; if we get a valid access token, we know who they are.” Explain exactly why that’s wrong and what they should use instead.
  2. 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?
  3. Walk through how PKCE defeats an intercepted authorization code, step by step, and explain why a stolen code on its own 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 get limited access to a user's resources on another service, such as reading their calendar or posting 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 giving an app permission to act, not about signing 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 to that client about who the user is. An access token is proof of permission, not proof of identity. Using a raw OAuth access token as a login is a well-known anti-pattern, and 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 for the user; it carries scopes and usually expires in minutes. A refresh token lasts longer, stays private to the client, and is exchanged at the authorization server for new access tokens when they expire, so the user doesn't have to approve again 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 with a user: web apps, single-page apps, and mobile apps. Client Credentials for machine-to-machine calls with no user. Device Code for devices with no keyboard, such as TVs and IoT. Refresh Token to renew access. Avoid the Implicit and Resource Owner Password Credentials grants entirely; OAuth 2.1 removes them.

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 creates a secret verifier, sends only its hash at the start, and reveals the verifier when it redeems the code, so an attacker who steals the authorization code can't exchange it without the secret. It began as a fix for mobile apps and is mandatory for every Authorization Code flow in OAuth 2.1.

What changed in OAuth 2.1?

OAuth 2.1 gathers a decade of best practice into one baseline: PKCE is mandatory for every Authorization Code flow, 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.