TRT
Sign in
Networking

OAuth 2.0 and OpenID Connect: Delegation Done Right

What the authorization code flow actually sends, why access and refresh tokens exist, and how OpenID Connect turns a token into 'who is this user?'.

The Runtime Theory Team8 min read#oauth#openid-connect#authentication#authorization#security

"Sign in with Google" is OAuth 2.0 wearing a trench coat. Underneath the smooth UX is a protocol with one job: let a third-party application act on a resource without ever seeing the user's password. OpenID Connect (OIDC) is the thin layer on top that answers the identity question OAuth 2.0 deliberately refuses to answer. Confusing the two is the source of most auth bugs.

The cast of characters

Four roles in OAuth 2.0 (RFC 6749):

  • Resource owner — the user.
  • Client — your application, which wants access.
  • Authorization server — the identity provider (IdP), e.g. Google, an Okta tenant, your own server, Keycloak.
  • Resource server — the API that actually holds the data (the Google Calendar API, your own backend).

The central design decision: the client never receives the user's password, and the client never even promises to hold on to the access token in plaintext. The client is treated as a semi-trusted party with limited powers.

The authorization code flow, message by message

For a web application this is the canonical flow. Every step is a request the client either redirects the user to or makes on their behalf:

  1. Client redirects the user to the authorization server:

    plaintext
    GET https://idp.example.com/authorize
        ?response_type=code
        &client_id=web-client
        &redirect_uri=https://app.example.com/callback
        &state=H7s0kf&scope=openid%20profile%20email

    The state parameter is your CSRF protection: it must be a cryptographically random value you generated and stored; you reject any callback whose state does not match.

  2. The user logs in and consents. The authorization server redirects back:

    plaintext
    https://app.example.com/callback?code=N78x2a&state=H7s0kf
  3. Only now does the client exchange the code, server-to-server, adding its client secret:

    plaintext
    POST /token
    Content-Type: application/x-www-form-urlencoded
    grant_type=authorization_code
    code=N78x2a
    redirect_uri=https://app.example.com/callback
    client_id=web-client
    client_secret=****

    The response is tokens: an access_token (short-lived, often 15–60 minutes) and a refresh_token (long-lived, days to months, and, for a public client like a mobile app, stored in the platform's secure enclave).

The two-step exchange exists precisely to keep the secret server-side. A native app or SPA that cannot hide a secret uses the same code flow but omits the client secret (PKCE instead provides a per-request verifier, as defined in RFC 7636); anyone who does the "implicit flow" of handing an access token back in the URL fragment is building on a flow that RFC 8252 explicitly deprecated.

Why two tokens?

  • The access token is a bearer credential for the resource server. Whatever it is — an opaque string or a JWT — the resource server must validate it before trusting the resource request. A JWT is handy precisely because the resource server can check it offline: signature, iss, aud, exp.
  • The refresh token exists so the client can mint new access tokens without making the user log in again. If a refresh token is stolen it is potent, which is why you rotate them: each refresh grants a new refresh token and invalidates the old one, and a reused (stolen) refresh token is a signal to revoke the session.

Where OpenID Connect adds "who"

OAuth 2.0 authorizes; it does not authenticate. When you need to know who the user is (not just what their token may access), you add the openid scope and the id_token: a JWT the client can verify, containing claims like sub (stable user id), email, email_verified, and nonce (the client's replay protection). The resource server's access-token check and the client's id-token check use the same signature keys — the IdP's JWKS endpoint — so:

plaintext
GET /jwks.json   →   { keys: [ { kid: "key-1", kty: "RSA", ... } ] }

A sane integration verifies: signature, iss matches your IdP, aud contains your client_id, exp in the future, and for the id_token, nonce equals what you sent. Checklist done, two minutes.

Mistakes that show up in production incidents

  • No state / nonce check. Your callback accepts a replay or a forged login. This is the CSRF gap that auditors flag first.
  • Trusting the access token on the client. The browser does not need the token's payload; it needs to send it. Parsing a JWT in JS "to get the email" means anyone with the token's signature keys is assumed — never parse on the client for authorization decisions.
  • Treating the refresh token as the access token. Refresh tokens must live in server-side storage or a secure enclave, be rotated, and be revocable. A refresh token in localStorage is a long-lived key to your whole account.

Key takeaways

  • OAuth 2.0 delegates access; OIDC authenticates identity. Never substitute one for the other.
  • Use the authorization code flow for anything that can hold a secret server-side; add PKCE for public clients.
  • Validate state on every redirect and iss/aud/exp/nonce on every JWT.
  • Rotate refresh tokens, and treat a reused refresh token as a stolen session.

Production cost model and latency numbers

To apply OAuth 2.0 and OpenID Connect: Delegation Done Right effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:

Operation / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, TCP/TLS framing, tail queueing ($L = \lambda W$)Connection pooling, deadline propagation, and p95 hedged requests

Failure modes, debugging, and operational checklist

In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on OAuth 2.0 and OpenID Connect: Delegation Done Right, walk through three verification questions:

  1. What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save