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

@@ -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);
}

View File

@@ -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<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 ───────────────────────────────────────────────
export async function createApiKey(db: Db = prodDb, name: string) {