Lion Cipher Lab

Design least-privilege OAuth scopes for an API -- and enforce them per route with tests

Why it matters

Most APIs that use OAuth start life with one scope, often literally "api", handed to every app. It works on day one, and it means a token leaked from the most harmless app (a read-only reporting dashboard, a partner integration) can approve invoices, export the database and add admin users. The OAuth security best practice (RFC 9700) says a token's privileges should be the minimum its use case needs, precisely so a leak hurts less. Doing that well is a design job: choosing scope names, deciding which client gets which, remembering that the person behind the token has limits of their own, and…

What you'll learn

Steps in this lab

  1. The words you need, in plain language
  2. The anti-pattern you are replacing: one scope called "api"
  3. Design the scope model on paper first
  4. Set up a folder and check Python
  5. Build the API, the tiny authorization server and the guard (api.py)
  6. Run it and ask it questions (call.py and mint.py)
  7. Pin the design down with tests (test_api.py)
  8. Measure the blast radius of a stolen token, per client (measure.py, section 1)
  9. Bug 1: a route missing from the map -- default-allow vs default-deny
  10. Bug 2: prefix matching lets invoices:read open invoices:read_all
  11. Bug 3: checking the app's scope but forgetting the person
  12. Judgment: how fine is fine enough, and when scopes are the wrong tool
  13. Honest negatives: what scopes do NOT do

Open the full interactive lab →