auth · Lesson 5 · Node + Express

OAuth2: The Authorization Code Flow

"Sign in with Google." "Connect your GitHub." You've used these a hundred times and watched the dizzying chain of redirects. Today that chain stops being magic. OAuth2 is a protocol for one app to get limited access to your stuff on another app — without ever seeing your password. The whole design hangs on one clever split, and you'll watch it run on your machine.

🎯 Mission: a mental model of modern auth + the ability to wire it into a real app
The one idea

OAuth2 splits the handoff across two channels so no single intercepted message is enough. A short-lived, near-useless authorization code travels through your browser (the exposed front channel). The powerful access token is then fetched server-to-server (the hidden back channel) using a client secret your browser never sees. The redirects you've always found confusing are the front channel doing its job.

01 What problem OAuth2 actually solves

End of Lesson 4 we hit the wall: passwords don't share. If MyApp wants to show your Google Photos, the terrible old way was to ask for your Google password and log in as you — handing a third party your master key, with no limits and no revocation. OAuth2 exists to kill that anti-pattern.

💡 OAuth2 is about delegated authorization Its native job: "let MyApp do this specific thing (read my photos) on my behalf, without my password, with a permission I can revoke." That's authorization (the "what may you do" from Lesson 1) — hence the name. Using it to log in ("Sign in with Google") is a layer on top called OIDC — that's Lesson 6. See oauth.net.

02 The cast (learn these four words and the flow reads itself)

Resource Owner

You. You own the photos and get to grant access.

Client

The app that wants in — "MyApp". Pre-registered with the provider; holds a client_id (public) and often a client_secret (private).

Authorization Server

Where you log in & consent — Google's/PhotoCloud's login. Issues codes and tokens.

Resource Server

The API holding your stuff (the Photos API). Accepts the access token.

Plus three things that get passed around: a scope (the specific permission, e.g. photos:read), an authorization code (a short-lived one-time ticket), and an access token (the actual bearer pass to the API — a token exactly like Lesson 3's).

03 The flow, five steps

Here's the entire authorization-code dance. Watch which arrow goes through the browser and which goes server-to-server — that's the whole trick.

You / Browser MyApp (Client) Provider ① "go ask the provider" (redirect) browser → /authorize?client_id&scope&state ② you log in + consent HERE ③ redirect back …/callback?code=ABC&state browser hands MyApp the code — everything above = FRONT CHANNEL (browser) — ④ POST /token code + client_secret 🔒 ← access_token — BACK CHANNEL (server↔server, no browser) — ⑤ GET /photos Bearer access_token → 200
Steps ①–③ ride the browser (front channel). Step ④ — the valuable one — is a direct server-to-server call carrying the client secret. The browser never touches the access token.

04 🎯 Your tangible win (4 min): run the flow, see the two channels

I put a zero-dependency simulation on your machine. It runs all five steps and prints which message crosses which channel — then shows an attacker who stole the browser-side code failing, because they lack the secret.

# in your terminal (no npm install needed):
cd ~/projects/learn/public/courses/auth/practice/oauth-lab
node demo.js
🎯 The win — the punchline, verbatim
STEP 3 (front channel): browser gets …/callback?code=2b83d6…&state=…
STEP 4 (back channel):  MyApp POSTs code + client_secret=•••••• → access_token

WHY TWO CHANNELS?
  attacker intercepts the front-channel CODE, tries to redeem it…
  🛡️ rejected → "bad client credentials"
  The code is worthless without the client_secret, which lives only on
  MyApp's server.
That rejection is the entire reason OAuth looks the way it does. The code is exposed (it's in a URL, in the browser, in logs) — and that's fine, because redeeming it requires a secret that never left the server. Open demo.js; every FRONT/BACK CHANNEL label maps to an arrow in the diagram above.

05 Why each weird bit exists

The confusing partWhy it's there
All the redirectsSo you authenticate on the provider's own site — MyApp never sees your password. The redirect is the security boundary.
A code first, then a token (two steps)The code can leak through the browser harmlessly; the token (which actually opens the API) is only ever fetched over the secret-protected back channel.
state parameterCSRF protection (Lesson 2!). MyApp sends a random state, checks it on return — so an attacker can't forge the callback.
Exact redirect_uri matchingThe provider only ever sends the code to a pre-registered URL, so it can't be redirected to an attacker's site.
scopeLeast privilege: MyApp gets photos:read, not your whole account. You see and consent to exactly that.

What it looks like in Express — the client side is just two routes (start + callback):

app.get('/connect', (req, res) => {                    // ① kick off
  req.session.oauthState = crypto.randomBytes(8).toString('hex');
  const u = new URL('https://provider.example/authorize');
  u.searchParams.set('response_type', 'code');
  u.searchParams.set('client_id', CLIENT_ID);
  u.searchParams.set('redirect_uri', 'https://myapp.example/callback');
  u.searchParams.set('scope', 'photos:read');
  u.searchParams.set('state', req.session.oauthState);
  res.redirect(u.toString());                              // → front channel
});

app.get('/callback', async (req, res) => {              // ③ provider redirected back
  if (req.query.state !== req.session.oauthState) return res.status(403).end(); // CSRF check
  const r = await fetch('https://provider.example/token', {  // ④ BACK channel
    method: 'POST',
    body: new URLSearchParams({ grant_type: 'authorization_code', code: req.query.code,
      client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: 'https://myapp.example/callback' }),
  });
  const { access_token } = await r.json();             // never reaches the browser
  // store access_token server-side, tied to this user's session (Lesson 2)
});

06 The one variant that changed everything (sets up Lesson 6)

This back-channel design assumes the client can keep a secret — true for a server (a "confidential client"). But a single-page app or mobile app has no secret: anything shipped to the browser/device can be extracted. No secret → step ④ can't be protected → a stolen code could be redeemed by anyone.

⚠️ Two now-banned shortcuts The old Implicit flow (skip the code, hand the token straight to the browser) and the Password grant (app collects your password directly) were OAuth2's answers and are now removed in OAuth 2.1 as insecure. The modern answer for public clients is PKCE — a dynamic, per-request secret that needs no pre-shared client secret. That's Lesson 6, and PKCE is now mandatory for everyone. See RFC 9700 (Jan 2025) · OAuth 2.1.

07 Check yourself

Q1. Why doesn't it matter much if the authorization code is exposed in the browser URL / server logs?

Show answer

Because redeeming the code for a token requires the client secret over the back channel — which the browser never has. The code alone is a single-use, short-lived ticket that's useless without the secret. You watched the attacker fail with "bad client credentials" for exactly this reason. (§04.) (This is also why public clients with no secret are a problem → PKCE.)

Q2. What's the state parameter for, and which earlier lesson does it connect to?

Show answer

CSRF protection (Lesson 2). MyApp generates a random state, stashes it in the session, sends it in step ①, and verifies the value coming back in step ③ matches. Without it, an attacker could trick your browser into completing a callback the attacker initiated. (§05.)

Q3. A junior dev says "let's just use the Implicit flow, it's simpler — the token comes straight back." Your response?

Show answer

Don't — the Implicit flow is removed in OAuth 2.1 because handing the access token through the front channel (browser/URL) exposes it to leakage with no back-channel protection. The modern approach is the authorization-code flow + PKCE, which is secure even without a client secret and is now mandatory. (§06.)

08 Where we go next

You can now read any OAuth2 flow and say what crosses which channel and why. Lesson 6 closes the two open threads: PKCE (how public apps with no secret do this safely — and you'll generate the verifier/challenge yourself), and OIDC (the thin layer that turns "access my photos" into "log me in" — i.e. "Sign in with Google" end to end, including the ID token, which is just a JWT from Lesson 3).

📌 Remember this one line OAuth2 = get limited access to your stuff on another site without your password, by splitting the handoff: a throwaway code through the browser (front channel) traded for an access token server-to-server (back channel) using a client secret the browser never sees.

💬 Ask me anything — e.g. "wire this against a real provider (GitHub) in Express", "what exactly is in the access token?", or "where do refresh tokens fit?" · Back to Lesson 4 · Resources.