Authentication answers who the user is; authorization answers what they may do. Most access control bugs come from the second being checked inconsistently, with one endpoint forgetting a check the others have.
One guard, used everywhere
Instead of repeating role checks in each handler, a single helper verifies the token and the role, and returns either the user or a ready-made error response.
type Role = "admin" | "seller" | "customer";
export async function requireRole(req: Request, ...roles: Role[]) {
const token = req.headers.get("authorization")?.replace("Bearer ", "");
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const user = await verifyToken(token);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
if (!roles.includes(user.role)) {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
return user;
}Using it in a handler
export async function DELETE(req: Request) {
const user = await requireRole(req, "admin");
if (user instanceof Response) return user;
// only admins reach this point
}Things to get right
- Verify the signature and expiry on the server on every request; never trust a decoded token alone.
- Check ownership as well as role: a seller should only edit their own products.
- Keep tokens short-lived and store them in httpOnly cookies when the client is a browser.