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
- Start here in System Design
- How a system design interview is graded
- The delivery framework: five phases and a clock
- Numbers to know: what one machine can really do
- System Design Concepts: CAP, SQL vs NoSQL, Message Queues
- SQL vs NoSQL: How to Choose a Database (System Design)
- Traffic Jumps 10×: What Breaks First? (System Design)
- Proxy vs Load Balancer vs API Gateway (System Design)
- CAP Theorem Explained Simply, with Examples
- Why Use a Message Queue? System Design Explained
- Message Queue vs Pub/Sub vs Kafka: Differences
- Polling vs Long Polling vs SSE vs Webhooks (System Design)
- API design in System Design
- REST vs GraphQL vs gRPC: Choosing an API (System Design)
- API Goals Canvas: Design APIs from the Consumer
- REST API Design: Resources, Methods, Status Codes
- HTTP Semantics, Richardson Maturity Model and HATEOAS
- GraphQL Explained: Over-fetching, Schema, N+1 Problem
- RPC and gRPC Explained: Protobuf, HTTP/2, When to Use
- API Design Best Practices: 9 Principles with Examples
- API Payload Design: Names, Types, Links and Errors
- Mapping Database Tables to API Resources, Step by Step
- API Design for Complex Schemas: Trees, Polymorphism, Bookings
- API Pagination, Idempotency Keys, Errors and Versioning
- Network-Efficient APIs: HTTP Caching, ETags, Field Selection
- Async APIs: 202 Accepted, Webhooks and Bulk Operations
- API Versioning and Evolution Without Breaking Clients
- API Contracts: OpenAPI, Contract Testing and Docs
- API Security: Authentication, Authorization, Rate Limiting
- OWASP API Top 10 Explained: BOLA, SSRF and More
- Running APIs: Gateways, Mesh, Rate Limits, Canaries
- API Troubleshooting Interview: Slow APIs and Unexpected Errors
- API Design Review: A Staff-Level Capstone
- Spotify Web API: Resources, IDs, Payloads and Paging
- Spotify Web API: OAuth PKCE, Scopes, Quotas, Rate Limits
- Spotify Web API Changes: Real API Evolution Lessons
- Database-heavy designs in System Design
- Design a Distributed Key-Value Store (System Design Interview)
- Design a Hotel Reservation System (System Design Interview)
- Design a Digital Wallet (System Design Interview)
- Design S3-like Object Storage (System Design Interview)
- Games in System Design
- Shard City: Learn Database Sharding by Playing a Game
- System Design Case Studies: Flash Sale, Chat, ChatGPT
- Flash Sale System Design: Who Gets the Last Item
- Food Delivery App System Design: Which Database?
- Cache Stampede: Why the Database Fell Over and the Fix
- Live Streaming System Design: Scaling for a Final
- ChatGPT System Design: Streaming LLM Responses
- RAG System Design: Product Q&A on Sale Day
- IRCTC Tatkal System Design: Booking Under Extreme Load
- Group Chat System Design: WebSockets and Fan-Out
- Authentication in System Design: Sessions, JWT, OAuth
- Authentication Methods Compared: Sessions, JWT, OAuth
- JWT in System Design: Sessions, Revocation, Refresh Tokens
- JWT Interview Questions and Answers
- Life of a session, step by step
- JWT Structure Explained: Header, Payload, Signature
- Forge lab: why verification must be strict
- JWT vs Session Authentication: Which to Use
- The validation pipeline: where a bad request dies
- How to Revoke a JWT: Blocklists and Short Expiry
- Refresh Tokens and Rotation Explained
- Logout in depth: one device, all devices, SSO
- JWT Security Best Practices
- OAuth 2.0 and OpenID Connect Explained
- Where to Store JWT: localStorage vs HttpOnly Cookie
- JWT in Microservices: API Gateway vs Service Validation
- Microservices, keys, and trust boundaries
- Sender-constrained tokens: making a stolen token useless
- Authentication vs Authorization Middleware
- Failure explorer: what breaks when X fails
- Capacity planner: sessions vs JWT vs hybrid
- Edge cases catalog
- Test it, measure it, migrate to it
- Anti-patterns to reject in a design review
- Design checklist and cheat sheet
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.