21

OAuth 2.0 & OpenID Connect

Letting an app act on your behalf without ever handing it your password — and telling authorization apart from authentication.

The delegated-authorization problem

Imagine an app wants to read your photos from a photo service. The naive way is to ask for your photo-service password and log in as you. That is a disaster: the app now knows your password, can do anything your account can (not just read photos), keeps that power forever, and if the app is breached your password leaks too. You also cannot revoke just this one app without changing your password everywhere.

Delegated authorization is the problem OAuth 2.0 solves: how do I let an app do one specific thing on my behalf, without giving it my password, in a way I can revoke at any time? The answer is to introduce a middle-man — the service that already holds your account — which authenticates you directly and then hands the app a limited, revocable token instead of your credentials.

OAuth 2.0 is the industry-standard protocol for exactly this delegation. The rest of this day is its moving parts.

   Naive (bad)                     OAuth (good)
   ───────────                     ────────────
   give app your password          app never sees password
   app = full account, forever     app = limited token, revocable
   breach leaks password           breach leaks a scoped token

The four roles

OAuth defines four participants. Naming them precisely makes every flow that follows readable, because each step is just a message between two of these roles.

RolePlain meaningIn the photo example
Resource ownerYou — the person who owns the data and grants accessThe user who owns the photos
ClientThe app that wants access on your behalfThe photo-printing app
Authorization serverThe service that authenticates you and issues tokensThe photo service's login/token system
Resource serverThe API holding the protected dataThe photo service's photo API

The authorization server and resource server are often run by the same company (the photo service is both), but they are separate roles: one proves who you are and issues tokens, the other serves data when shown a valid token. Keeping them distinct is what lets one login system guard many APIs.

   Resource owner (you)
        │ grants access
        ▼
   Client (the app) ──asks──► Authorization server ──issues token──►
        │                                                    │
        └──────── uses token to call ──► Resource server ◄───┘

Tokens and scopes

OAuth replaces your password with tokens — strings that grant limited access. There are two kinds, and the difference between them is lifespan and purpose.

  • Access token — the key the client presents to the resource server on each API call. It is deliberately short-lived (minutes to an hour), so that if it leaks, it expires quickly. The client sends it in an HTTP header.
  • Refresh token — a longer-lived credential the client uses to get a new access token when the old one expires, without bothering you to log in again. It is more sensitive (it can mint access tokens) so it is stored carefully and never sent to the resource server.

A scope is the mechanism for limited access: it names exactly what the token is allowed to do. When the client requests access, it asks for specific scopes (photos.read), and you see and approve exactly those. The token then carries only that permission — it cannot delete photos or read your messages.

GET /api/photos HTTP/1.1
Host: api.photoservice.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

The design intent: a short-lived, narrowly-scoped access token limits the damage of any leak in time and in power, while the refresh token keeps the experience smooth by renewing access silently.

The Authorization Code flow with PKCE

This is the main, recommended OAuth flow — the one you should reach for by default. It is designed so the access token is never exposed to the browser's address bar or to any untrusted party. Two ideas make it work.

First, the authorization code: instead of handing the client a token directly, the authorization server hands it a short-lived, one-time code, which the client then exchanges for the real token over a secure back-channel. The code is useless on its own — it must be traded in.

Second, PKCE (Proof Key for Code Exchange, pronounced "pixy"): a safeguard that stops an attacker who steals the code from using it. Before starting, the client invents a random secret — the code verifier — and sends only a hashed version of it — the code challenge — when it asks for the code. Later, to exchange the code for a token, it must present the original verifier. Only the client that started the flow knows it, so a stolen code cannot be redeemed by anyone else.

Walk the steps: you click connect; the client sends you to the authorization server with a code challenge; you authenticate and approve the scopes; you are redirected back to the client carrying a one-time code; the client exchanges that code plus its secret verifier for tokens over a direct server-to-server call; the client then uses the access token to fetch your data. At no point did the client see your password, and a stolen code is worthless without the verifier.

   User      Client                 Auth server        Resource server
    │  click "connect"  │                │                    │
    │──────────────────►│                │                    │
    │        redirect w/ code_challenge  │                    │
    │◄──────────────────┤───────────────►│                    │
    │   log in + approve requested scopes│                    │
    │═══════════════════════════════════►│                    │
    │        redirect back with CODE     │                    │
    │◄═══════════════════════════════════┤                    │
    │                   │  exchange: CODE + code_verifier      │
    │                   │───────────────►│                    │
    │                   │  access + refresh token              │
    │                   │◄───────────────┤                    │
    │                   │        call API with access token    │
    │                   │─────────────────────────────────────►│
    │                   │              protected data          │
    │                   │◄─────────────────────────────────────┤

Why implicit and password grants are deprecated

OAuth originally offered simpler flows that are now discouraged. Understanding why they were retired reinforces what the code-plus-PKCE flow protects.

  • Implicit grant. This returned the access token directly in the redirect URL, skipping the code exchange. The token ended up in the browser's address bar and history, where it could leak — and there was no PKCE-style proof, so an intercepted token was immediately usable. The Authorization Code flow with PKCE now gives browser and mobile apps the same simplicity without exposing the token, so implicit is obsolete.
  • Resource owner password credentials (password) grant. This had the user type their password directly into the client, which forwarded it to the authorization server. That defeats the entire point of OAuth — the client sees the password, so all the naive-approach dangers return. It only ever existed for legacy migration and is now strongly discouraged.

The through-line: both retired flows either exposed the token or exposed the password. Modern OAuth's guidance is simple — use the Authorization Code flow with PKCE for anything a user logs into.

The client credentials flow

Not every OAuth exchange involves a person. When one service needs to call another with no user in the loop — a backend job calling an internal API — there is a simpler flow: client credentials.

Here the client is the resource owner: it authenticates as itself using its own credentials (a client ID and secret it holds securely) and receives an access token directly. There is no browser redirect, no login page, no user consent, because there is no user — just one trusted service proving its own identity to get a scoped token for another service.

POST /oauth/token HTTP/1.1
Host: auth.internal.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=svc-billing&client_secret=...&scope=invoices.read

Use this for service-to-service (machine-to-machine) access, and only there — because it relies on a stored secret, which is safe for a locked-down backend but never for an app running on a user's device where the secret could be extracted.

OAuth 2.0 versus OpenID Connect

Here is the single most common confusion in this whole area, and it is worth stating bluntly. OAuth 2.0 is about authorization, not authentication. It answers "may this app do X on my behalf?" It was never designed to answer "who is this user?" — an access token tells the resource server the bearer is allowed to do something, not who they are.

OpenID Connect (OIDC) is a thin layer built on top of OAuth 2.0 that adds authentication. It reuses the same Authorization Code flow but adds one thing: an ID token, a token whose entire job is to tell the client who the user is (their identity — a stable user id, and optionally name and email). This is what powers "Sign in with Google": Google's OIDC layer gives the app a verified statement of who you are.

OAuth 2.0OpenID Connect
Question it answersWhat may this app do? (authZ)Who is this user? (authN)
Main tokenAccess tokenID token (plus access token)
Token is forThe resource server (an API)The client (to learn the user's identity)
Use it forGranting an app access to your dataLogging users in with an existing account

The rule to remember: use plain OAuth when you want an app to access resources; use OIDC when you want an app to know who the user is. Using a bare access token as proof of identity is a classic security mistake — it was not built to carry that meaning.

JWT structure and validation

The ID token, and often access tokens, take the form of a JWT — a JSON Web Token. A JWT is a compact, self-contained token made of three Base64-encoded parts joined by dots: a header, a payload, and a signature. "Self-contained" is the key trait — it carries its own data (called claims) and its own proof of authenticity, so the receiver can validate it without calling back to the issuer.

Decoded, the first two parts are just readable JSON:

{
  "alg": "RS256",
  "typ": "JWT"
}
{
  "iss": "https://auth.example.com",
  "sub": "user-4815162342",
  "aud": "photo-print-app",
  "exp": 1720545600,
  "iat": 1720542000,
  "scope": "photos.read",
  "email": "alex@example.com"
}

The payload's claims carry the meaning: iss (issuer — who made it), sub (subject — the user's stable id), aud (audience — who it is for), exp (expiry time), iat (issued-at time), and application claims like scope or email. The third part, the signature, is a cryptographic seal the issuer makes with a private key; anyone can verify it with the issuer's public key.

Crucially, the header and payload are only encoded, not encrypted — anyone can read them. The signature does not hide the contents; it proves they were not tampered with. Validating a JWT therefore means checking, in order: the signature is valid (it really came from the issuer and was not altered), exp has not passed (not expired), iss is who you expect, and aud includes you. Skipping any of these — especially trusting the payload without checking the signature — is how forged tokens get accepted.

   header . payload . signature
   ──────   ───────   ─────────
   xxxxx  . yyyyy   . zzzzz

Token expiry and rotation

Tokens do not live forever, and their short lives are a feature, not an inconvenience. This ties back to the two token types and to the broader security habit of limiting how long any leaked credential stays useful.

The access token expires quickly (its exp claim), so a stolen one is soon worthless. When it expires, the client silently trades its refresh token for a fresh access token — the user notices nothing. Rotation hardens this further: each time a refresh token is used, the authorization server can issue a new refresh token and invalidate the old one. If an attacker steals a refresh token and uses it, the legitimate client's next use fails (the token was already rotated), which signals the theft so the server can revoke the whole chain.

The layered result: access tokens keep leaks brief, refresh tokens keep the experience seamless, and rotation turns a stolen refresh token from a lasting compromise into a detectable, revocable event.

   access token  ── short life ──►  expires  ──► use refresh token
        ▲                                              │
        └───────── new access token ◄──────────────────┘
   refresh rotation: old refresh invalidated on each use
                     → reuse reveals theft

Key takeaways

  • OAuth 2.0 solves delegated authorization: let an app do one scoped thing on your behalf without your password, revocably.
  • The four roles — resource owner (you), client (the app), authorization server (issues tokens), resource server (holds data) — make every flow a message between two of them.
  • Access tokens are short-lived keys for the resource server; refresh tokens silently renew them; scopes limit exactly what a token may do.
  • The Authorization Code flow with PKCE is the default: a one-time code exchanged over a back-channel, with a secret verifier so a stolen code is useless.
  • Implicit and password grants are deprecated because they exposed the token or the password respectively; PKCE made them unnecessary.
  • Client credentials is the no-user, service-to-service flow — use it only for locked-down backends that can hold a secret.
  • OAuth is authorization ("what may this app do?"); OpenID Connect adds authentication ("who is this user?") via an ID token — never use a bare access token as proof of identity.
  • A JWT is header.payload.signature — encoded, not encrypted; validate the signature, expiry, issuer, and audience before trusting it.
  • Short-lived access tokens plus refresh-token rotation keep leaks brief and make a stolen refresh token detectable and revocable.

Checklist

  • [ ] I can explain the delegated-authorization problem and why sharing a password is unacceptable.
  • [ ] I can name the four OAuth roles and place each in a flow.
  • [ ] I can distinguish access tokens, refresh tokens, and scopes by lifespan and purpose.
  • [ ] I can walk through the Authorization Code flow with PKCE and explain what the code and verifier protect.
  • [ ] I can explain why the implicit and password grants are deprecated.
  • [ ] I can describe when to use the client credentials flow and its one constraint.
  • [ ] I can clearly separate OAuth 2.0 (authorization) from OpenID Connect (authentication) and the role of the ID token.
  • [ ] I can describe a JWT's three parts and the checks required to validate one.
  • [ ] I can explain token expiry and refresh-token rotation and what each buys.