A valid token only says who is calling; your API still has to decide what they may do.
After validation, the API holds trustworthy claims. Authorization happens in layers:
- Authentication middleware validates the token once per request and attaches the principal (
sub, client id, scopes, roles) to the request context. - Coarse checks per route: does the token carry the scope this operation needs?
GET /ordersneedsorders.read,POST /ordersneedsorders.write. Return 403 witherror="insufficient_scope"when the token is valid but lacks a scope, and 401 when the token is missing or invalid. - Fine-grained checks per resource: scopes say what the client may do on the user's behalf, not what the user may do. The API must still check that order 123 belongs to
subor that the user's role allows the action. Skipping this produces broken object-level authorization (the top API vulnerability in the OWASP API Security Top 10).
Practical guidance:
- Keep scopes coarse and stable; model detailed permissions in your own database or a policy engine, not as hundreds of scopes.
- Distinguish user tokens from service tokens (client credentials have no user
subin the usual sense). - Log the
sub, client id and decision for audits, never the token itself.
Example
// Express-style middleware (framework-agnostic idea)
function requireScope(scope) {
return (req, res, next) => {
const granted = (req.auth?.scope ?? "").split(" ");
if (!req.auth) return res.status(401).set("WWW-Authenticate", 'Bearer').end();
if (!granted.includes(scope)) {
return res.status(403)
.set("WWW-Authenticate", `Bearer error="insufficient_scope", scope="${scope}"`).end();
}
next();
};
}
// Route: scope check AND ownership check
// app.get("/orders/:id", requireScope("orders.read"), async (req, res) => {
// const order = await db.orders.find(req.params.id);
// if (!order || order.ownerSub !== req.auth.sub) return res.status(404).end();
// res.json(order);
// });
console.log(typeof requireScope("orders.read")); // functionRemember
- 401 = no or invalid token; 403 = valid token, not allowed
- Scopes limit the client; resource checks limit the user
- Always verify the caller owns or may access the specific object
Watch out for
- Trusting scopes alone and letting any user with
orders.readread everyone's orders.