Roles are coarse (admin, customer); scopes are fine-grained permissions (messages:read, refunds:write). The JWT carries both. Security(scopes=["refunds:write"]) declares what the route needs; the route depends on get_current_user which returns the typed User with role and scopes. Missing scope = 403.
Token claims: {”sub”: user.id, ”role”: “customer”, "scopes": ["messages:read"]}.
Route: @router.post(“/refunds”, dependencies=[Security(verify_scope, scopes=[“refunds:write”])]).
verify_scope checks if any scope in the JWT contains the required string.
JWT scopes -> Security(scopes=[...]) -> verify_scope dependency -> route
Missing scope -> 403
Scopes give per-endpoint granularity on top of role. Same JWT, different gate.