OAuth 2, OpenID Connect and Scopes for APIs

Learn OAuth 2 for APIs with labs: authorization code with PKCE, client credentials, scope design, validating JWT access tokens, OIDC, DPoP and mTLS.

OAuth 2 authorization for APIs: common questions

Why does a mobile app need PKCE in OAuth?

A mobile app can’t keep a client secret, and another app can intercept the authorization code during the redirect. With PKCE the app sends only a hash of a random verifier at the start, and the code can be swapped for tokens only by whoever holds the original verifier.

Do OAuth scopes replace ownership checks in an API?

No. A scope records what the user let an app do on their behalf, such as bookings:write. It never says which booking, so the service must still check that the resource belongs to the token’s user.

Related chapters

Keep learning

Overview

Lab: a fake app on John’s phone grabs the authorization code Code flow without PKCE Code flow with PKCE URL carries: client_id=tripplan & redirect_uri=tripplan://callback\nNo secret: TripPlan is a mobile app, anyone can unpack it and read a “secret”. 1. Malware registered tripplan:// and receives ?code=SplxlO 2. Attacker posts the code to /token first POST /token\ngrant_type=authorization_code&code=SplxlO&client_id=tripplan\n&code_verifier=??? ← the attacker never saw it POST /token\ngrant_type=authorization_code&code=SplxlO&client_id=tripplan\n&redirect_uri=tripplan://callback 400 invalid_grant 200 access_token for John The code is a bearer credential: whoever redeems it first gets the tokens. The attacker now reads and cancels John’s bookings, and the real app gets invalid_grant because codes are single use. ) : stolen ? The code travelled through the browser and a redirect, places an app does not control. Now see who can turn it into a token. GET /events?city=Pune GET /me/bookings POST /bookings DELETE /bookings/9 Booking 9 belongs to Priya, and the token’s sub is John. GET /venues/12/payouts Payouts belong to venue 12’s manager, Asha. John is a customer. Lab: John granted TripPlan some scopes. Which calls work? Scopes on the consent screen (tap to grant or remove) TripPlan calls the ticket API with John’s token 201 Created 401 invalid_token 403 insufficient_scope

OAuth 2, OpenID Connect and scopes for APIs

A travel app wants to read John’s ticket bookings. OAuth lets John say yes without handing over his password, and gives the ticket API a token that says exactly which app may do what, for whom, and for how long. This chapter is about that token: how it is issued safely, how scopes are designed, and what the API must check before it trusts one.

The delegation problem: why OAuth exists

Before OAuth: share the password TripPlan asks John for his ticket-site password and logs in as him. It can now do everything John can, forever, including payments. John can’t revoke TripPlan without changing his password, and the ticket site can’t tell TripPlan’s calls from John’s. With OAuth: a scoped, revocable token John logs in on the ticket site itself and approves “TripPlan may read your bookings”. TripPlan gets a token for that one job. It expires, John can revoke it in his settings, and every call is labelled with TripPlan’s client_id. The previous chapter (

Four roles, two kinds of server

Resource owner The person whose bookings they are. Gives or refuses consent. TripPlan app The app that wants access. Registered with a client_id and allowed redirect URLs. Authorization server Logs John in, shows consent, issues access and refresh tokens. Resource server The split is the point. Login, consent screens, MFA and token signing live in one place (often a product such as Keycloak, Auth0 or Okta). Every API behind it only has to answer one question per request: is this token valid for me and this route? Clients must be registered first, with what the consent screen and the server need to know: name, logo for the consent screen, exact redirect URLs, and which scopes it may ever request.

Authorization code flow with PKCE, step by step

John’s browser TripPlan app Auth server Ticket API make code_verifier, code_challenge, state GET /authorize?response_type=code&client_id=tripplan&scope=bookings:read&state=af0i&code_challenge=E9Mr…&code_challenge_method=S256 app opens the browser at the auth server John logs in, sees “TripPlan wants to read your bookings”, taps Allow the password is typed only here 302 → tripplan://callback?code=SplxlO&state=af0i short-lived, single-use code through the browser state matches? POST /token code=SplxlO&code_verifier=dBjf… back channel, straight to the auth server SHA-256(verifier) == stored challenge → access_token (10 min) + refresh_token GET /me/bookings Authorization: Bearer eyJ… API validates the token on every call Why the extra hop through a code? The browser redirect is a leaky channel: URLs end up in history, logs and, on phones, in any app that registered the same URL scheme. So the redirect carries only a code that is useless by itself, and the real tokens come back on a direct HTTPS call. PKCE (RFC 7636) closes the remaining gap: the code is also bound to a secret only the app that started the flow holds. state guards the other direction. An attacker could start a flow with their own account and trick John’s browser into finishing it, so TripPlan links John’s profile to the attacker’s bookings (login CSRF). TripPlan stores a random state per login and drops any callback whose state it didn’t issue. PKCE also blocks this when the server enforces it, but state is cheap and still common. The JWT topic (opens in a new tab) animates the same flow from the login side.

Other grants: machines, TVs, refresh, and what to stop using

user present browser on device use it today Authorization code + PKCE Client credentials Device authorization Refresh token Resource owner password Which grant fits which caller Client credentials is for machine to machine with no user: PromoHub, an affiliate site, lists Pune events from its backend every hour. It authenticates as itself and gets a token whose subject is the client, not a person. Prefer this over a static API key when you already run an authorization server: tokens expire in minutes and carry scopes, so a leak has a short life. Two grants are on the way out. Implicit returned the access token straight in the redirect URL, exactly the leaky channel the code flow avoids. Resource owner password has the app collect the user’s password, the very thing OAuth exists to avoid. The OAuth 2.0 Security Best Current Practice (RFC 9700) says not to use them, and the OAuth 2.1 draft drops both and makes PKCE the norm for the code flow.

Designing scopes: fine, coarse, and what they don’t do

Fine-grained: one scope per action events:read, bookings:read, bookings:write, bookings:cancel, payouts:read…Least privilege, but a consent screen with twelve lines nobody reads, and every new endpoint needs a new scope. Coarse-grained: one scope per job read, write, adminEasy to explain, but “write” for a calendar app also lets it book and cancel. Every client ends up over-privileged. Most APIs land in between: scopes named resource:action around what a client is trying to achieve, sized so a typical partner asks for two or three. Keep money and personal data in their own scopes (payouts:read should never ride along with events:read), and write the required scope next to each operation in the OpenAPI contract so docs, gateway and tests share one map. OWASP API Top 10

What the API checks on every access token

issuer’s JWKS, by kid exact match this API exp / nbf small clock skew per route in the service The API downloads the authorization server’s public keys from its JWKS URL (found in its /.well-known/openid-configuration), caches them, and picks one by the token’s kid. When an unknown kid shows up, it refetches once (keys rotate) and then gives up, so a stream of junk tokens can’t make it hammer the auth server.

Opaque or JWT access tokens, and where OpenID Connect fits

Opaque token + introspection A random string. The API asks the auth server about it (RFC 7662) and gets back active, scope, sub, exp. Revocation is instant and nothing leaks to the client, but every call (or cache miss) costs a network hop to the auth server. JWT access token (RFC 9068) Self-contained and signed, header typ: at+jwt. The API validates it locally with cached keys, so the auth server is off the hot path. A revoked token stays usable until exp, so keep lifetimes short (5 to 15 minutes). A common split: JWTs inside your own platform for speed, opaque tokens handed to outside partners so they can’t read or depend on the claims, with the gateway introspecting and swapping one for the other. ID token (OpenID Connect) Access token (OAuth)

Sender-constrained tokens for high-value APIs

Bearer token Like cash: whoever holds it can spend it. A token copied from a log line, a proxy or a compromised partner works from any machine until it expires. Sender-constrained token Like a card plus PIN: the token names a key (claim cnf), and each call must prove the caller holds that key. A copied token alone is refused.

Where checks run, and the interview line

API gateway: coarse checks once, at the edge Valid signature, iss, aud, exp, and “route POST /bookings needs bookings:write”. Rejects junk before it costs a service anything, and applies per-client rate limits by client_id. Service: fine checks needs the data Is booking 9 John’s? Is venue 12 Asha’s? Is this refund under the limit for this client? Only the service has the rows to answer, so it also re-reads sub and scope from the token. cover running that edge. We now know how a well-behaved caller gets in. Next, the OWASP API Top 10 looks at how attackers get in anyway, starting with the ownership check this chapter kept pointing at. Spotify’s real PKCE login: accounts.spotify.com/authorize, code_challenge_method=S256, one-hour tokens. PKCE with Spotify Its scope catalogue and a least-privilege lab. Spotify scopes

More courses

  • Jobs, linked to lessons — Browse jobs where every requirement links to the chapter that teaches it. See how ready you are, how long the gap takes to close, and which listings look stale.
  • Match a job post to courses — Paste a job post. Each skill is matched to a track here and put in learning order, foundations first.
  • React from open source with patterns — Patterns extracted from Material UI, Radix, React Aria, React Hook Form, Redux Toolkit, TanStack, and Next.js — not slides about hooks.
  • Database in depth — How a query becomes work. Indexes, isolation, locks, and the plans that explain why a page is slow — not an ORM tour.
  • Build a real API with NestJS — One ticket-booking API, built in order: a plain Node server first, then NestJS modules, validation, a real database, and the features real clients lean on.
  • Microservices — Microservice patterns one diagram at a time: how to split a monolith, how services talk, and how data stays consistent without distributed transactions.
  • Kafka — A beginner-to-senior path through event logs, reliable delivery, production operations, and disaster recovery.
  • AI engineering for software engineers — Eight free reads, in order, for software engineers moving into AI: when to build an agent, how to build one, how to test it, and how the model works underneath.
  • Frontend system design — How a UI is sliced when it is a product, not a page: ownership of state, cache, routes, and what happens when the network lies.
  • TypeScript in depth — The type system as a design tool. Inference, narrowing, generics, and the errors that mean the model is wrong — not a catalog of utility types.
  • CSS in depth — See the box, then the page. Visual chapters from the box model through Flexbox surprises, Grid, selectors, and units, then backgrounds, shapes, and visual effects, each with a playground.
  • Accessibility — Foundations first (WCAG in plain words, semantic HTML, lists, alt text), then hands-on HTML labs, a full pass of WAI-ARIA 1.3, and a final fix-the-page challenge.
  • Docker — What an image is, why layers cache, and how containers find each other on a network.
  • Redis — Fifteen Redis patterns, one picture each: cache, session, lock, counter, rate limiter, queue, and when Redis is the wrong tool.
  • Kubernetes — Kubernetes from zero to operators: why it exists, Pods, Deployments and Services, then configuration, networking, storage, scheduling, scaling and security.