/** * Gates routes on a session token, and on a permission after it. The routers * put these middleware before a handler; a route open to anonymous callers * calls `parseOptionalAuth` (`lib/auth.ts`) instead. * * @module */ import { type Next, type RouteParams, type RouterContext, type State, } from "@oak/oak"; import { authenticate } from "../lib/auth.ts"; import { APIException } from "../lib/api-exception.ts"; import type { AuthPayload } from "../model/auth.ts"; import { can } from "../lib/permissions.ts"; import type { Permission } from "../../shared/permissions.ts"; /** * Set on `ctx.state` by {@link authMiddleware} for the handlers after it. * `user` carries the caller's live role, not the one in their token. */ export interface AuthState extends State { user: AuthPayload; } /** * The context type of a handler behind {@link authMiddleware}. Pass the route * path, as in `AuthContext<"/:key/role">`, to keep `ctx.params` typed. */ export type AuthContext< R extends string = string, P extends RouteParams = RouteParams, > = RouterContext; /** * Requires a valid session token in the `Authorization` header, and puts its * payload in `ctx.state.user` for the rest of the route. * * @throws {APIException} 401 when the token is missing or invalid. */ export async function authMiddleware( ctx: AuthContext, next: Next, ): Promise { const authHeader = ctx.request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new APIException(401, "Missing or invalid token"); } const token = authHeader.substring(7); const payload = await authenticate(token); if (!payload) { throw new APIException(401, "Invalid token"); } ctx.state.user = payload; await next(); } /** * Returns a middleware that requires the caller to hold `permission`. It must * run after {@link authMiddleware}, as in * `router.patch(path, authMiddleware, requirePermission("user:manage"), …)`. * The returned middleware throws an {@link APIException} (403) when the caller * lacks the permission. */ export function requirePermission( permission: Permission, ): (ctx: AuthContext, next: Next) => Promise { return async ( ctx: AuthContext, next: Next, ): Promise => { if (!ctx.state.user || !can(ctx.state.user, permission)) { throw new APIException(403, "Insufficient permissions"); } await next(); }; }