awesome-everything RU
↑ Back to the climb

Security

OAuth/OIDC: code review

Crux Read real OAuth/OIDC code — PKCE exchange, id_token validation, refresh rotation, and DPoP binding — spot the mandatory check that is missing or inverted, and pick the fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

Every OAuth CVE is one missing or inverted check, hiding in code that looks correct. Read each snippet the way a security reviewer would, find the skipped mandatory step, and choose the fix that closes the actual hole.

Goal

Practise the review loop that catches OAuth bugs before they ship: read the exchange or validation path, identify which mandatory check is absent or wrong, and reach for the precise fix rather than a vague hardening pass.

Snippet 1 — the token exchange

// callback handler after the IdP redirects back with ?code=...&state=...
async function handleCallback(req, session) {
  const { code, state } = req.query;
  const res = await fetch(TOKEN_ENDPOINT, {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      code_verifier: session.codeVerifier,
    }),
  });
  return res.json(); // tokens
}
Quiz

The PKCE code_verifier is sent correctly. What mandatory check is missing in this callback, and what does it let an attacker do?

Snippet 2 — id_token validation

import jwt from "jsonwebtoken";

function verifyIdToken(idToken, jwks) {
  const header = decodeHeader(idToken);
  const key = jwks.find((k) => k.kid === header.kid);
  // verify using whatever the token says it used
  const claims = jwt.verify(idToken, key.pem, { algorithms: [header.alg] });
  if (claims.iss !== EXPECTED_ISS) throw new Error("bad iss");
  if (claims.exp < Date.now() / 1000) throw new Error("expired");
  return claims;
}
Quiz

This validator checks iss and exp. Which two mandatory checks are broken or missing, and why do they matter?

Snippet 3 — refresh rotation

async function refresh(presentedRT) {
  const record = await db.refreshTokens.find(presentedRT);
  if (!record) throw new Error("unknown refresh token");
  // issue a fresh pair
  const next = await mintTokens(record.userId);
  await db.refreshTokens.insert(next.refreshToken, { userId: record.userId });
  return next;
}
Quiz

This issues a new refresh token on every call. Why does it fail to detect a stolen refresh token, and what is the fix?

Snippet 4 — DPoP proof verification

function verifyDpop(proofJwt, accessToken, req) {
  const { header, payload } = parse(proofJwt);
  verifySignature(proofJwt, header.jwk); // signed by embedded key
  if (payload.htm !== req.method) throw new Error("htm");
  if (payload.htu !== req.url) throw new Error("htu");
  if (Math.abs(now() - payload.iat) > 60) throw new Error("iat skew");
  return true;
}
Quiz

This checks the proof signature, htm, htu, and iat. Which two DPoP bindings are missing, and what attack does each gap allow?

Recap

Every snippet failed the same way real OAuth code does — by skipping or inverting one mandatory check while looking correct: a missing state comparison opens login CSRF; trusting header.alg and dropping aud/nonce opens algorithm confusion, audience confusion, and replay; minting without used-marking defeats rotation; and a DPoP verifier without ath, the cnf.jkt thumbprint match, and jti replay-caching is not actually sender-constrained. Review for the complete set: read the path, name the absent check, apply the precise fix.

Continue the climb ↑OAuth/OIDC: build and audit a hardened login
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.