Implementing Authorization
1. Creating Role-Based Middleware
Example: requireRole factory
export const requireRole = (...roles) => (req, res, next) => {
if (!req.user) return next(new HttpError(401));
if (!roles.includes(req.user.role)) return next(new HttpError(403));
next();
};
app.delete("/admin/users/:id", requireAuth, requireRole("admin"), removeUser);
2. Checking User Roles
| Source | Where stored |
|---|---|
| JWT claim | req.user.role after verify |
| Session | req.session.user.role |
| Database | Re-fetch on each request (always-fresh, slower) |
3. Implementing Permission Checks
Example: Permission helper
const PERMS = {
admin: ["user:*", "post:*"],
editor: ["post:read", "post:write"],
user: ["post:read"]
};
export function hasPermission(role, perm) {
return (PERMS[role] || []).some(p => p === perm || p.endsWith(":*") && perm.startsWith(p.slice(0, -1)));
}
export const can = (perm) => (req, res, next) =>
hasPermission(req.user?.role, perm) ? next() : next(new HttpError(403));
4. Using Access Control Lists
Example: ACL with accesscontrol
import AccessControl from "accesscontrol";
const ac = new AccessControl();
ac.grant("user").readOwn("post").createOwn("post");
ac.grant("admin").extend("user").readAny("post").deleteAny("post");
const permission = ac.can(req.user.role).readAny("post");
if (!permission.granted) throw new HttpError(403);
5. Protecting Resource Access
Example: Owner-or-admin
app.delete("/posts/:id", requireAuth, async (req, res, next) => {
const post = await db.posts.findById(req.params.id);
if (!post) return next(new HttpError(404));
if (post.authorId !== req.user.sub && req.user.role !== "admin") {
return next(new HttpError(403));
}
await db.posts.remove(req.params.id);
res.sendStatus(204);
});
6. Implementing Attribute-Based Control (ABAC)
Example: Policy function
// policies/post.js
export const canEditPost = (user, post) =>
user.role === "admin" ||
(post.authorId === user.sub && !post.locked && post.status !== "published");
// route
if (!canEditPost(req.user, post)) throw new HttpError(403);
| Model | Decision based on |
|---|---|
| RBAC | Role only |
| ABAC | User + resource + environment attributes |
| ReBAC | Relationships (Zanzibar / OpenFGA / Cerbos) |
7. Creating Authorization Middleware Chain
Example: Compose
app.patch("/orgs/:orgId/projects/:id",
requireAuth,
requireOrgMember(), // sets req.membership
requireRole("owner","editor"),
loadProject(), // sets req.project
authorize((req) => canEditProject(req.user, req.project)),
updateProject
);
8. Handling Unauthorized Access
| Status | Meaning |
|---|---|
| 401 Unauthorized | Missing or invalid credentials — challenge with WWW-Authenticate |
| 403 Forbidden | Authenticated but not allowed — do not retry |
Example: 401 vs 403
if (!req.user) {
res.set("WWW-Authenticate", 'Bearer realm="api"');
throw new HttpError(401);
}
if (!hasPermission(req.user.role, "post:delete")) throw new HttpError(403);
9. Using Scopes for API Access
Example: OAuth scopes
export const requireScope = (...scopes) => (req, res, next) => {
const token = req.user?.scope?.split(" ") || [];
const ok = scopes.every(s => token.includes(s));
ok ? next() : next(new HttpError(403, "Insufficient scope"));
};
app.get("/me/email", requireAuth, requireScope("read:email"), getEmail);
10. Implementing Resource-Level Permissions
Example: Per-row check
// Filter at query time — never trust client to filter
const posts = await prisma.post.findMany({
where: {
OR: [
{ authorId: req.user.sub },
{ collaborators: { some: { userId: req.user.sub } } },
...(req.user.role === "admin" ? [{}] : [])
]
}
});
Warning: Always enforce row-level access in the database query, not in JS after fetching. Otherwise IDOR vulnerabilities arise.