Files
skills/docs/api-stack/api/api-plugin-auth/src/middleware/require-role.md
2026-05-29 08:20:56 +01:00

2.6 KiB

middleware/require-role

Purpose

requireRole(...roles) builds a Hono middleware that gates a route on the principal carrying at least one of the supplied roles. The factory throws an AuthError(AUTH_INVALID_INPUT) at construction time when called with no roles — a guard against accidentally building a no-op middleware.

Features

  • Constant-time membership check against an internal Set<string>.
  • Two failure modes — AUTH_UNAUTHENTICATED (401) for anonymous callers, AUTH_INSUFFICIENT_ROLE (403) for callers whose principal.roles does not intersect the required set.
  • Emits auth.role_check_total{outcome} for every call, with outcome ∈ "allowed" | "denied".
  • Carries context.required and context.actual on the 403 error so audit logs / OpenAPI responses can show the precise mismatch.

Usage: Basic

import { requireAuth, requireRole } from "@suaveplan/api-plugin-auth/middleware";

app.use("/admin/*", requireAuth(), requireRole("admin"));

Usage: Advanced

Compose multiple roles for resource-level scoping. Combine with AuthService lookups to project derived roles (e.g. organisation membership) onto the request before the middleware runs:

import { requireAuth, requireRole } from "@suaveplan/api-plugin-auth/middleware";

app.use("/projects/:id/edit", requireAuth(), async (c, next) => {
    const principal = c.get("user");
    if (!principal) return next();
    const isOwner = await checkOwnership(c.req.param("id"), principal.id);
    if (isOwner) {
        c.set("user", { ...principal, roles: [...principal.roles, "owner"] });
    }
    await next();
}, requireRole("admin", "owner"));

API Reference

function requireRole(...roles: readonly string[]): MiddlewareHandler

  • Throws AuthError(AUTH_INVALID_INPUT) at construction when roles is empty.
  • Throws AuthError(AUTH_UNAUTHENTICATED) (401) when c.get("user") is null/undefined.
  • Throws AuthError(AUTH_INSUFFICIENT_ROLE) (403) when no role in principal.roles is in the required set.
  • Emits auth.role_check_total{outcome="allowed"|"denied"} on every request.

Implementation Notes

  • The required-roles set is constructed once per factory call. Reusing the same middleware across multiple routes is constant memory.
  • The error context carries the actual roles as well as the required ones so the 403 response can surface a "missing role: admin" message in operator logs without re-reading the principal.
  • The middleware never inspects principal.tenantId — tenant-scoped role gates are the responsibility of @suaveplan/api-tenant's middleware, layered downstream.