Engineering Atlas › System Design Fundamentals

Delegated access with OAuth 2.0

11 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Roles

The resource owner is the user. The client is the app requesting access. The authorization server (AS) authenticates the user, asks for consent and issues tokens. The resource server (RS) is the API that accepts tokens. Scopes name the permissions requested, such as calendar.read.

Authorization code flow with PKCE

  1. The client generates a random code_verifier and sends the user's browser to the AS with client_id, redirect_uri, scope, state and code_challenge = BASE64URL(SHA256(code_verifier)) with code_challenge_method=S256 (RFC 7636 §4.2). The plain method, where the challenge equals the verifier, exists only for clients that cannot hash; GitHub, for one, refuses it (GitHub docs).
  2. The user logs in and consents. The AS redirects back to redirect_uri with a one-time code and the same state.
  3. The client checks state (CSRF protection), then calls the AS token endpoint directly with the code and the original code_verifier.
  4. The AS checks the code is unused, unexpired (typically 60 seconds), bound to this client and redirect URI, and that the verifier hashes to the stored challenge. It returns an access token and maybe a refresh token.

The code travels through the browser, where it can leak via logs or malicious apps. PKCE makes a stolen code useless without the verifier, which never left the client. The OAuth Security BCP, RFC 9700 (January 2025), says public clients MUST use PKCE and confidential clients SHOULD; clients SHOULD NOT use the implicit flow; and the resource owner password grant, where the app collects the user's password itself, MUST NOT be used (RFC 9700 §2). OAuth 2.1, which folds these rules into one document and requires PKCE for all clients using the code flow, was still an Internet-Draft (revision 16, September 2026) at the time of writing (draft-ietf-oauth-v2-1).

Other grants

Client credentials: a backend authenticates as itself (secret, mTLS or signed JWT) and gets a token, no user involved. Device authorization (RFC 8628): a TV shows a short code, the user approves on a phone, and the TV polls, every 5 seconds unless told otherwise, backing off when the server answers slow_down. Refresh token: exchange for a new access token; with rotation, each use returns a new refresh token and reuse of an old one signals theft and revokes the family. RFC 9700 requires refresh tokens issued to public clients to be either rotated or sender-constrained.

Token formats and validation

Access tokens are either opaque (the RS calls the AS introspection endpoint, RFC 7662) or self-contained JWTs (the RS checks the signature with the AS's published keys, plus iss, aud, exp and scopes). JWTs avoid a network hop per request but cannot be revoked before expiry, so keep them short-lived (5 to 15 minutes). Opaque tokens can be revoked instantly but put the AS on the hot path.

Down the stack

Layer 1The contract

The authorization server promises to issue tokens only after authenticating the user and recording consent for the requested scopes. An authorization code is single-use, short-lived (RFC 6749 recommends at most 10 minutes; 30 to 60 seconds is common), and bound to the client and redirect URI that requested it (RFC 6749 §4.1.2). If a code is redeemed twice, RFC 9700 says the server should revoke every token already issued from it, because one of the two callers is an attacker. A resource server promises to honour a token only if its signature, issuer, audience, expiry and scopes all check out. Plain access tokens are bearer tokens: whoever holds one can use it, which is the property every other defence works around.

Layer 2The mechanism

The token request is a form-encoded POST: grant_type=authorization_code, code, redirect_uri, code_verifier, plus client authentication for confidential clients. The JSON response holds access_token, token_type: Bearer, expires_in, an optional refresh_token and the granted scope. A JWT access token is three base64url segments, header, claims and signature; the header names the algorithm (ES256, RS256) and a key id kid, which the resource server looks up in the JSON Web Key Set published at the server's jwks_uri. Refresh token rotation stores tokens in families: each refresh issues a child and invalidates the parent, and presenting an already-used token revokes the whole family. Sender-constrained tokens bind a token to a key the client holds, so a stolen token alone is useless. With DPoP the client signs a small JWT per request, covering the HTTP method, URL and a hash of the token, and the token carries a cnf claim with the key's thumbprint (RFC 9449); with mutual TLS the binding is to the client certificate (RFC 8705). A client that talks to several authorization servers also needs mix-up defence: the AS returns an iss parameter on the redirect and the client compares it with the server it started with (RFC 9207).

Layer 3The machine

Verifying an ES256 signature costs roughly 50 to 100 microseconds of CPU and RS256 verification about 20 to 50; RSA signing is the expensive side, around a millisecond for a 2048-bit key, which only the authorization server pays. A JWT with a few claims is 800 to 1,500 bytes and rides in every request's Authorization header; HTTP/2 header compression can index it, but proxies with 8 KB header limits reject tokens bloated with group lists. The PKCE verifier is 32 random bytes from the kernel CSPRNG (getrandom), 43 characters once base64url-encoded, giving 256 bits an attacker cannot guess. Expiry checks compare against the local clock, so servers allow a minute or two of skew and depend on NTP.

Why it's built this way

  1. Users should not give passwords to every app, so a trusted server issues limited tokens instead.
  2. The only channel between an arbitrary app and the authorization server is a browser redirect, which leaks easily, so the redirect carries a short-lived code and a back-channel exchange plus PKCE turns it into tokens.
  3. Many resource servers at high request rates cannot all call the authorization server, so tokens are signed and self-contained.
  4. Self-contained tokens cannot be recalled, so they are kept short-lived and renewed with refresh tokens.
  5. Bearer tokens work like cash, so high-value APIs bind tokens to client keys.

Diagram

sequenceDiagram
    participant U as "User browser"
    participant C as "Client app"
    participant AS as "Authorization server"
    participant RS as "Resource API"
    C->>U: redirect with code_challenge, state
    U->>AS: /authorize
    AS->>U: login + consent
    U->>C: redirect_uri?code=X&state=s
    C->>AS: POST /token code=X, code_verifier
    AS-->>C: access_token (JWT, 10 min), refresh_token
    C->>RS: GET /events, Bearer token
    RS->>RS: verify signature, aud, exp, scope
    RS-->>C: 200 events

Worked example

A SaaS product has 2 million users, 600,000 daily active, and an API serving 25,000 requests per second.

Opaque tokens with introspection would add 25,000 introspection calls per second to the AS, each 2 to 5 ms, and make the AS a single point of failure for every API call. A cache of introspection results for 60 seconds cuts this to roughly one call per active token per minute, but delays revocation by up to a minute.

Instead the team uses signed JWT access tokens with a 10-minute lifetime. API gateways verify ES256 signatures locally (around 50 microseconds each) using keys fetched from the AS's JWKS endpoint and cached. Each active session refreshes about every 10 minutes: 600,000 users active for an average 2 hours is 7.2 million refreshes per day, about 83 per second on average and maybe 400 at peak. That is the AS's real load.

Revocation: when an admin disables a user, their refresh token is revoked immediately, and the access token lives at most 10 more minutes. High-risk endpoints also check a small deny list of revoked sessions.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
JWT access tokensLocal validation, no AS hopRevocation waits for expiryHigh-QPS APIs, many resource servers
Opaque tokens + introspectionInstant revocation, no data in tokenAS on hot path, extra latencyLower QPS, high-risk actions
Refresh token rotationDetects stolen refresh tokensRace conditions on parallel refreshPublic clients (SPAs, mobile)
Client credentialsSimple service authNo user contextMachine-to-machine calls

Pitfalls in production

Leadership lens

Check yourself

  1. What attack does PKCE stop that state does not?

    Show answer

    Authorization code interception: an attacker who steals the code cannot redeem it without the code verifier. state only stops cross-site request forgery of the callback.

  2. Why are JWT access tokens kept short-lived?

    Show answer

    Resource servers validate them locally and cannot see revocation, so expiry is the main bound on how long a leaked or revoked token works.

  3. Why is OAuth alone not a login protocol?

    Show answer

    An access token says what a client may do at an API, not who the user is or that they just authenticated; OIDC's ID token carries that.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →