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:
6
bun.lock
6
bun.lock
@@ -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=="],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user