Skip to content

Access control and optional authentication

Authentication establishes who the caller is. Authorization decides what that caller may do. Always authenticate with protect() before applying role or permission middleware.

Role-based access control

authorize() allows a user whose single req.user.role matches any role in the allow-list:

ts
app.get(
  "/admin",
  auth.protect(),
  auth.authorize(["admin"]),
  adminHandler,
);

app.get(
  "/reports",
  auth.protect(),
  auth.authorize(["admin", "manager", "auditor"]),
  reportsHandler,
);

Use roles for broad application areas. A missing role or a non-matching role returns 403 AUTH_FORBIDDEN.

Permission-based access control

Use authorizePermissions() for fine-grained checks. The user must have every listed permission in the JWT permissions claim:

ts
app.patch(
  "/users/:id",
  auth.protect(),
  auth.authorizePermissions(["users:read", "users:update"]),
  updateUserHandler,
);

Recommended permission names are explicit and resource-oriented:

text
users:read
users:create
users:update
users:delete
reports:export

Use separate middleware calls when a route has separate authorization rules. Permission checks do not replace resource ownership checks; the handler must still verify that the user may access the specific record.

Combining roles and permissions

Use both checks when a route needs a broad role and a specific capability:

ts
app.delete(
  "/users/:id",
  auth.protect(),
  auth.authorize(["admin", "support"]),
  auth.authorizePermissions(["users:delete"]),
  deleteUserHandler,
);

Middleware runs from left to right. If req.user is missing because protect() was omitted or mounted later, the authorization middleware returns 401 AUTH_UNAUTHORIZED.

Optional authentication

Use optional() when guests and signed-in users can both access an endpoint:

ts
app.get("/articles/:slug", auth.optional(), (req, res) => {
  res.json({
    article: getArticle(req.params.slug),
    bookmarked: Boolean(req.user),
  });
});

optional() never rejects a request. It sets req.user when a valid token is present and leaves it undefined for guests or invalid tokens.

Testing authorization

Test at least these paths:

  • No token: 401 AUTH_TOKEN_MISSING from protect().
  • No req.user: 401 AUTH_UNAUTHORIZED from authorization middleware.
  • Wrong role or missing permission: 403 AUTH_FORBIDDEN.
  • Matching role or all required permissions: handler executes.
  • Authenticated user with a forbidden resource ID: handler still rejects it.