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
This commit is contained in:
2026-04-04 20:43:52 +02:00
parent f7c9f3dc94
commit 259dc2bc8c
4 changed files with 24 additions and 125 deletions

View File

@@ -5,6 +5,7 @@
"": { "": {
"name": "gearbox", "name": "gearbox",
"dependencies": { "dependencies": {
"@hono/oidc-auth": "^1.8.1",
"@hono/zod-validator": "^0.7.6", "@hono/zod-validator": "^0.7.6",
"@modelcontextprotocol/sdk": "^1.29.0", "@modelcontextprotocol/sdk": "^1.29.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
@@ -14,6 +15,7 @@
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"hono": "^4.12.8", "hono": "^4.12.8",
"jose": "^6.2.2",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^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/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=="], "@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=="], "@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=="], "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-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],

View File

@@ -35,6 +35,7 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"@hono/oidc-auth": "^1.8.1",
"@hono/zod-validator": "^0.7.6", "@hono/zod-validator": "^0.7.6",
"@modelcontextprotocol/sdk": "^1.29.0", "@modelcontextprotocol/sdk": "^1.29.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
@@ -44,6 +45,7 @@
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"hono": "^4.12.8", "hono": "^4.12.8",
"jose": "^6.2.2",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",

View File

@@ -1,21 +1,12 @@
import type { Context, Next } from "hono"; import type { Context, Next } from "hono";
import { getCookie } from "hono/cookie"; import { getAuth } from "@hono/oidc-auth";
import { import { verifyApiKey } from "../services/auth.service";
getSession, import { verifyAccessToken } from "../services/oauth.service";
getUserCount,
refreshSession,
verifyApiKey,
} from "../services/auth.service";
export async function requireAuth(c: Context, next: Next) { export async function requireAuth(c: Context, next: Next) {
const db = c.get("db"); const db = c.get("db");
// Check if any users exist at all // 1. Check API key (programmatic access)
if (getUserCount(db) === 0) {
return c.json({ error: "setup_required" }, 403);
}
// Check API key first
const apiKey = c.req.header("X-API-Key"); const apiKey = c.req.header("X-API-Key");
if (apiKey) { if (apiKey) {
const valid = await verifyApiKey(db, 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); return c.json({ error: "Invalid API key" }, 401);
} }
// Check session cookie // 2. Check MCP OAuth Bearer token
const sessionId = getCookie(c, "gearbox_session"); const authHeader = c.req.header("Authorization");
if (sessionId) { if (authHeader?.startsWith("Bearer ")) {
const session = getSession(db, sessionId); const token = authHeader.slice(7);
if (session) { if (await verifyAccessToken(db, token)) return next();
// Refresh session expiry on use return c.json({ error: "invalid_token" }, 401);
refreshSession(db, sessionId);
return next();
}
} }
// 3. Check OIDC session (browser users)
const auth = await getAuth(c);
if (auth) return next();
return c.json({ error: "Authentication required" }, 401); return c.json({ error: "Authentication required" }, 401);
} }

View File

@@ -1,111 +1,10 @@
import { randomBytes } from "node:crypto"; 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 { 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; 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<boolean> {
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 ─────────────────────────────────────────────── // ── API Key Management ───────────────────────────────────────────────
export async function createApiKey(db: Db = prodDb, name: string) { export async function createApiKey(db: Db = prodDb, name: string) {