From 259dc2bc8c4acd2764d10757aab6a0cd99f0e9e0 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 4 Apr 2026 20:43:52 +0200 Subject: [PATCH] feat(15-02): install OIDC deps, rewrite auth middleware and service - Install @hono/oidc-auth and jose for OIDC integration - Rewrite requireAuth middleware with three-way auth: API key, MCP Bearer, OIDC session - Strip auth.service.ts to API key functions only (remove user/session management) - Remove all references to getUserCount, getSession, refreshSession from middleware --- bun.lock | 6 ++ package.json | 2 + src/server/middleware/auth.ts | 36 ++++------ src/server/services/auth.service.ts | 105 +--------------------------- 4 files changed, 24 insertions(+), 125 deletions(-) diff --git a/bun.lock b/bun.lock index 4240ec4..4d5cb8d 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "gearbox", "dependencies": { + "@hono/oidc-auth": "^1.8.1", "@hono/zod-validator": "^0.7.6", "@modelcontextprotocol/sdk": "^1.29.0", "@tailwindcss/vite": "^4.2.1", @@ -14,6 +15,7 @@ "drizzle-orm": "^0.45.1", "framer-motion": "^12.38.0", "hono": "^4.12.8", + "jose": "^6.2.2", "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -166,6 +168,8 @@ "@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="], + "@hono/oidc-auth": ["@hono/oidc-auth@1.8.1", "", { "dependencies": { "oauth4webapi": "^2.6.0" }, "peerDependencies": { "hono": ">=3.0.0" } }, "sha512-EK95ilPVeX4O+oWIOe/DyhdodA7ckUiH9uP0mMpLLXnpv1b364QRX01EJFNl4QRn5kjcl2OZ+jgb6vde5kBV6A=="], + "@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -656,6 +660,8 @@ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "oauth4webapi": ["oauth4webapi@2.17.0", "", {}, "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], diff --git a/package.json b/package.json index 281ba01..8a1b3bd 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "typescript": "^5.9.3" }, "dependencies": { + "@hono/oidc-auth": "^1.8.1", "@hono/zod-validator": "^0.7.6", "@modelcontextprotocol/sdk": "^1.29.0", "@tailwindcss/vite": "^4.2.1", @@ -44,6 +45,7 @@ "drizzle-orm": "^0.45.1", "framer-motion": "^12.38.0", "hono": "^4.12.8", + "jose": "^6.2.2", "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts index b9e36de..42ca70c 100644 --- a/src/server/middleware/auth.ts +++ b/src/server/middleware/auth.ts @@ -1,21 +1,12 @@ import type { Context, Next } from "hono"; -import { getCookie } from "hono/cookie"; -import { - getSession, - getUserCount, - refreshSession, - verifyApiKey, -} from "../services/auth.service"; +import { getAuth } from "@hono/oidc-auth"; +import { verifyApiKey } from "../services/auth.service"; +import { verifyAccessToken } from "../services/oauth.service"; export async function requireAuth(c: Context, next: Next) { const db = c.get("db"); - // Check if any users exist at all - if (getUserCount(db) === 0) { - return c.json({ error: "setup_required" }, 403); - } - - // Check API key first + // 1. Check API key (programmatic access) const apiKey = c.req.header("X-API-Key"); if (apiKey) { const valid = await verifyApiKey(db, apiKey); @@ -23,16 +14,17 @@ export async function requireAuth(c: Context, next: Next) { return c.json({ error: "Invalid API key" }, 401); } - // Check session cookie - const sessionId = getCookie(c, "gearbox_session"); - if (sessionId) { - const session = getSession(db, sessionId); - if (session) { - // Refresh session expiry on use - refreshSession(db, sessionId); - return next(); - } + // 2. Check MCP OAuth Bearer token + const authHeader = c.req.header("Authorization"); + if (authHeader?.startsWith("Bearer ")) { + const token = authHeader.slice(7); + if (await verifyAccessToken(db, token)) return next(); + return c.json({ error: "invalid_token" }, 401); } + // 3. Check OIDC session (browser users) + const auth = await getAuth(c); + if (auth) return next(); + return c.json({ error: "Authentication required" }, 401); } diff --git a/src/server/services/auth.service.ts b/src/server/services/auth.service.ts index 1ed003e..8ad9633 100644 --- a/src/server/services/auth.service.ts +++ b/src/server/services/auth.service.ts @@ -1,111 +1,10 @@ import { randomBytes } from "node:crypto"; -import { count, eq } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { db as prodDb } from "../../db/index.ts"; -import { apiKeys, sessions, users } from "../../db/schema.ts"; +import { apiKeys } from "../../db/schema.ts"; type Db = typeof prodDb; -// ── User Management ────────────────────────────────────────────────── - -export async function createUser( - db: Db = prodDb, - username: string, - password: string, -) { - const passwordHash = await Bun.password.hash(password); - return db.insert(users).values({ username, passwordHash }).returning().get(); -} - -export async function verifyPassword( - db: Db = prodDb, - username: string, - password: string, -) { - const user = db - .select() - .from(users) - .where(eq(users.username, username)) - .get(); - - if (!user) return null; - - const valid = await Bun.password.verify(password, user.passwordHash); - return valid ? user : null; -} - -export function getUserCount(db: Db = prodDb): number { - const result = db.select({ value: count() }).from(users).get(); - return result?.value ?? 0; -} - -export async function changePassword( - db: Db = prodDb, - username: string, - currentPassword: string, - newPassword: string, -): Promise { - const user = await verifyPassword(db, username, currentPassword); - if (!user) return false; - - const newHash = await Bun.password.hash(newPassword); - db.update(users) - .set({ passwordHash: newHash }) - .where(eq(users.id, user.id)) - .run(); - - return true; -} - -// ── Session Management ─────────────────────────────────────────────── - -export function createSession( - db: Db = prodDb, - userId: number, - expiryDays = 30, -) { - const id = randomBytes(32).toString("hex"); - const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000); - - return db - .insert(sessions) - .values({ id, userId, expiresAt }) - .returning() - .get(); -} - -export function getSession(db: Db = prodDb, sessionId: string) { - const session = db - .select() - .from(sessions) - .where(eq(sessions.id, sessionId)) - .get(); - - if (!session) return null; - - if (session.expiresAt < new Date()) { - db.delete(sessions).where(eq(sessions.id, sessionId)).run(); - return null; - } - - return session; -} - -export function deleteSession(db: Db = prodDb, sessionId: string) { - db.delete(sessions).where(eq(sessions.id, sessionId)).run(); -} - -export function refreshSession( - db: Db = prodDb, - sessionId: string, - expiryDays = 30, -) { - const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000); - db.update(sessions) - .set({ expiresAt }) - .where(eq(sessions.id, sessionId)) - .run(); -} - // ── API Key Management ─────────────────────────────────────────────── export async function createApiKey(db: Db = prodDb, name: string) {