Lion Cipher Lab

MCP server, Part 3: protect it with OAuth 2.1 — validate bearer access tokens, check the audience, and enforce a scope (no paid API key or cloud IdP needed)

Why it matters

A static shared token (Part 2) has one fatal limit: everyone who can call the server holds the same secret, it never expires, and the server has no idea WHO is calling or whether the token was even meant for it. The moment your MCP server has more than one caller — third-party clients, multiple users, tokens minted by some other service — that model breaks, and it breaks in the exact way the MCP threat model warns about: the confused deputy, where your server accepts a token that was minted for a DIFFERENT service and unwittingly acts on an attacker's behalf (token passthrough). OAuth 2.1…

What you'll learn

Steps in this lab

  1. What changed since Part 2 — from a shared secret to real OAuth 2.1
  2. The five words: access token, JWT, signature, audience, scope (and JWKS)
  3. Audience restriction and the confused-deputy / token-passthrough threat (RFC 8707)
  4. Protected Resource Metadata (RFC 9728) — how a client discovers your auth server
  5. When OAuth is worth it — and when Part 2's static token is the right call
  6. Set up: reuse the environment and add PyJWT
  7. Build your stand-in authorization server: mint.py
  8. Write the resource server: validate signature + expiry + audience, then enforce scope
  9. Prove every check with curl: metadata, 401 challenge, 200, and two 403s
  10. The failure mode to avoid: accepting any signed JWT without checking `aud`
  11. Do it better: a real IdP (Keycloak), key rotation, and short-lived tokens

Open the full interactive lab →