From ec61b3ae4a69841fd8a584fc7af7f201f291b370 Mon Sep 17 00:00:00 2001 From: Paperclip Date: Fri, 27 Mar 2026 20:39:42 +0000 Subject: [PATCH] feat(api): replace JWT auth with Better-Auth session validation (GRO-118) - Replace jose/jwtVerify with auth.api.getSession() - Session token validated via cookie/header, DB-backed - jwtPayload.sub now = Better-Auth user ID (not OIDC sub) - Dev mode bypass preserved; production guard against AUTH_DISABLED preserved - rbac.ts and tests updated in subsequent tasks Co-Authored-By: Paperclip --- apps/api/src/middleware/auth.ts | 59 +++++++++++---------------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts index 44f4100..66ec3d4 100644 --- a/apps/api/src/middleware/auth.ts +++ b/apps/api/src/middleware/auth.ts @@ -1,34 +1,18 @@ import type { MiddlewareHandler } from "hono"; -import { createRemoteJWKSet, jwtVerify } from "jose"; +import { auth } from "../lib/auth.js"; -// Authentik OIDC configuration — loaded from env at startup -const OIDC_ISSUER = process.env.OIDC_ISSUER; -const OIDC_AUDIENCE = process.env.OIDC_AUDIENCE; - -let jwks: ReturnType | null = null; - -function getJwks() { - if (!OIDC_ISSUER) throw new Error("OIDC_ISSUER is not set"); - if (!jwks) { - jwks = createRemoteJWKSet( - new URL(`${OIDC_ISSUER}/application/o/groombook/jwks/`) - ); - } - return jwks; +export interface AuthUser { + id: string; + email: string; + name: string; } -export interface JwtPayload { - sub: string; - email?: string; - name?: string; -} - -// Guard: refuse to start with AUTH_DISABLED in production (fixes #22). +// Guard: refuse to start with AUTH_DISABLED in production. if (process.env.AUTH_DISABLED === "true") { if (process.env.NODE_ENV === "production") { console.error( "[FATAL] AUTH_DISABLED=true is not allowed in production. " + - "Remove AUTH_DISABLED from your environment and configure OIDC_ISSUER." + "Remove AUTH_DISABLED from your environment and configure Better-Auth." ); process.exit(1); } @@ -42,27 +26,24 @@ export const authMiddleware: MiddlewareHandler = async (c, next) => { if (process.env.AUTH_DISABLED === "true") { const devUserId = c.req.header("X-Dev-User-Id"); const sub = devUserId ?? "dev-user"; - c.set("jwtPayload", { sub } as JwtPayload); + c.set("jwtPayload", { sub } as { sub: string }); await next(); return; } - const authorization = c.req.header("Authorization"); - if (!authorization?.startsWith("Bearer ")) { + const session = await auth.api.getSession({ + headers: c.req.raw.headers, + }); + + if (!session) { return c.json({ error: "Unauthorized" }, 401); } - const token = authorization.slice(7); - - try { - const { payload } = await jwtVerify(token, getJwks(), { - issuer: OIDC_ISSUER, - audience: OIDC_AUDIENCE, - }); - - c.set("jwtPayload", payload as JwtPayload); - await next(); - } catch { - return c.json({ error: "Invalid or expired token" }, 401); - } + // Set jwtPayload with sub = Better-Auth user ID for backward compat with resolveStaffMiddleware + c.set("jwtPayload", { + sub: session.user.id, + email: session.user.email, + name: session.user.name, + }); + await next(); };