feat(15-02): rewrite auth routes for OIDC login/callback/logout
- Add top-level /login, /callback, /logout OIDC routes in index.ts - Strip auth.ts to /me (OIDC claims) and API key CRUD only - Remove credential-based login, setup, password change routes - Remove all cookie/session handling from auth routes
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { serveStatic } from "hono/bun";
|
import { serveStatic } from "hono/bun";
|
||||||
import { cors } from "hono/cors";
|
import { cors } from "hono/cors";
|
||||||
|
import {
|
||||||
|
oidcAuthMiddleware,
|
||||||
|
processOAuthCallback,
|
||||||
|
revokeSession,
|
||||||
|
} from "@hono/oidc-auth";
|
||||||
import { db as prodDb } from "../db/index.ts";
|
import { db as prodDb } from "../db/index.ts";
|
||||||
import { seedDefaults } from "../db/seed.ts";
|
import { seedDefaults } from "../db/seed.ts";
|
||||||
import { mcpRoutes } from "./mcp/index.ts";
|
import { mcpRoutes } from "./mcp/index.ts";
|
||||||
@@ -35,6 +40,14 @@ app.get("/api/health", (c) => {
|
|||||||
return c.json({ status: "ok" });
|
return c.json({ status: "ok" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── OIDC Browser Auth (top-level, before /api/* middleware) ───────────
|
||||||
|
app.get("/login", oidcAuthMiddleware(), async (c) => c.redirect("/"));
|
||||||
|
app.get("/callback", async (c) => processOAuthCallback(c));
|
||||||
|
app.get("/logout", async (c) => {
|
||||||
|
await revokeSession(c);
|
||||||
|
return c.redirect("/login");
|
||||||
|
});
|
||||||
|
|
||||||
// CORS for OAuth and MCP endpoints (required for claude.ai browser-based flows)
|
// CORS for OAuth and MCP endpoints (required for claude.ai browser-based flows)
|
||||||
app.use("/.well-known/*", cors());
|
app.use("/.well-known/*", cors());
|
||||||
app.use("/oauth/*", cors());
|
app.use("/oauth/*", cors());
|
||||||
|
|||||||
@@ -1,165 +1,37 @@
|
|||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { eq } from "drizzle-orm";
|
import { getAuth } from "@hono/oidc-auth";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { users } from "../../db/schema.ts";
|
|
||||||
import { parseId } from "../lib/params.ts";
|
import { parseId } from "../lib/params.ts";
|
||||||
import { requireAuth } from "../middleware/auth.ts";
|
import { requireAuth } from "../middleware/auth.ts";
|
||||||
import { rateLimit } from "../middleware/rateLimit.ts";
|
|
||||||
import {
|
import {
|
||||||
changePassword,
|
|
||||||
createApiKey,
|
createApiKey,
|
||||||
createSession,
|
|
||||||
createUser,
|
|
||||||
deleteApiKey,
|
deleteApiKey,
|
||||||
deleteSession,
|
|
||||||
getSession,
|
|
||||||
getUserCount,
|
|
||||||
listApiKeys,
|
listApiKeys,
|
||||||
verifyPassword,
|
|
||||||
} from "../services/auth.service.ts";
|
} from "../services/auth.service.ts";
|
||||||
|
|
||||||
type Env = { Variables: { db?: any } };
|
type Env = { Variables: { db?: any } };
|
||||||
|
|
||||||
const loginSchema = z.object({
|
|
||||||
username: z.string().min(1),
|
|
||||||
password: z.string().min(1),
|
|
||||||
});
|
|
||||||
const setupSchema = z.object({
|
|
||||||
username: z.string().min(1),
|
|
||||||
password: z.string().min(6),
|
|
||||||
});
|
|
||||||
const changePasswordSchema = z.object({
|
|
||||||
currentPassword: z.string().min(1),
|
|
||||||
newPassword: z.string().min(6),
|
|
||||||
});
|
|
||||||
const createKeySchema = z.object({ name: z.string().min(1) });
|
const createKeySchema = z.object({ name: z.string().min(1) });
|
||||||
|
|
||||||
const COOKIE_NAME = "gearbox_session";
|
|
||||||
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
|
|
||||||
|
|
||||||
const app = new Hono<Env>();
|
const app = new Hono<Env>();
|
||||||
|
|
||||||
// ── Public routes ───────────────────────────────────────────────────
|
// ── Auth Status ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
app.get("/me", (c) => {
|
app.get("/me", async (c) => {
|
||||||
const db = c.get("db");
|
const auth = await getAuth(c);
|
||||||
const sessionId = getCookie(c, COOKIE_NAME);
|
if (auth) {
|
||||||
|
return c.json({
|
||||||
if (sessionId) {
|
user: { id: auth.sub, email: auth.email },
|
||||||
const session = getSession(db, sessionId);
|
authenticated: true,
|
||||||
if (session) {
|
});
|
||||||
return c.json({
|
|
||||||
user: { id: session.userId },
|
|
||||||
setupRequired: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return c.json({ user: null, authenticated: false });
|
||||||
const setupRequired = getUserCount(db) === 0;
|
|
||||||
return c.json({ user: null, setupRequired });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/setup", rateLimit, zValidator("json", setupSchema), async (c) => {
|
// ── API Key Management (protected) ───────────────────────────────────
|
||||||
const db = c.get("db");
|
|
||||||
|
|
||||||
if (getUserCount(db) > 0) {
|
app.get("/keys", requireAuth, async (c) => {
|
||||||
return c.json({ error: "Setup already completed" }, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { username, password } = c.req.valid("json");
|
|
||||||
const user = await createUser(db, username, password);
|
|
||||||
const session = createSession(db, user.id);
|
|
||||||
|
|
||||||
setCookie(c, COOKIE_NAME, session.id, {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: "Lax",
|
|
||||||
path: "/",
|
|
||||||
maxAge: COOKIE_MAX_AGE,
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({ username: user.username }, 201);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/login", rateLimit, zValidator("json", loginSchema), async (c) => {
|
|
||||||
const db = c.get("db");
|
|
||||||
const { username, password } = c.req.valid("json");
|
|
||||||
|
|
||||||
const user = await verifyPassword(db, username, password);
|
|
||||||
if (!user) {
|
|
||||||
return c.json({ error: "Invalid credentials" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = createSession(db, user.id);
|
|
||||||
|
|
||||||
setCookie(c, COOKIE_NAME, session.id, {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: "Lax",
|
|
||||||
path: "/",
|
|
||||||
maxAge: COOKIE_MAX_AGE,
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({ username: user.username });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/logout", (c) => {
|
|
||||||
const db = c.get("db");
|
|
||||||
const sessionId = getCookie(c, COOKIE_NAME);
|
|
||||||
|
|
||||||
if (sessionId) {
|
|
||||||
deleteSession(db, sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteCookie(c, COOKIE_NAME, { path: "/" });
|
|
||||||
return c.json({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Protected routes ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
app.put(
|
|
||||||
"/password",
|
|
||||||
requireAuth,
|
|
||||||
zValidator("json", changePasswordSchema),
|
|
||||||
async (c) => {
|
|
||||||
const db = c.get("db");
|
|
||||||
const sessionId = getCookie(c, COOKIE_NAME);
|
|
||||||
if (!sessionId) {
|
|
||||||
return c.json({ error: "Session required for password change" }, 401);
|
|
||||||
}
|
|
||||||
const session = getSession(db, sessionId);
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return c.json({ error: "Session required for password change" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userRecord = db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.id, session.userId))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!userRecord) {
|
|
||||||
return c.json({ error: "User not found" }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentPassword, newPassword } = c.req.valid("json");
|
|
||||||
const changed = await changePassword(
|
|
||||||
db,
|
|
||||||
userRecord.username,
|
|
||||||
currentPassword,
|
|
||||||
newPassword,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!changed) {
|
|
||||||
return c.json({ error: "Invalid current password" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ ok: true });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
app.get("/keys", requireAuth, (c) => {
|
|
||||||
const db = c.get("db");
|
const db = c.get("db");
|
||||||
const keys = listApiKeys(db);
|
const keys = listApiKeys(db);
|
||||||
return c.json(keys);
|
return c.json(keys);
|
||||||
@@ -186,11 +58,11 @@ app.post(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.delete("/keys/:id", requireAuth, (c) => {
|
app.delete("/keys/:id", requireAuth, async (c) => {
|
||||||
const db = c.get("db");
|
const db = c.get("db");
|
||||||
const id = parseId(c.req.param("id"));
|
const id = parseId(c.req.param("id"));
|
||||||
if (!id) return c.json({ error: "Invalid key ID" }, 400);
|
if (!id) return c.json({ error: "Invalid key ID" }, 400);
|
||||||
deleteApiKey(db, id);
|
await deleteApiKey(db, id);
|
||||||
return c.json({ ok: true });
|
return c.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user