From ae920aa3476437fa633d81e72fc3394e7604934f Mon Sep 17 00:00:00 2001 From: "groombook-engineer[bot]" <3141748+groombook-engineer[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:29:27 +0000 Subject: [PATCH] fix(GRO-424): move reinitAuth to active router, add SSRF timeout, fix trailing slash - Add reinitAuth() import and calls to routes/authProvider.ts (active router) instead of routes/admin/authProvider.ts (dead code, not imported) - Add AbortSignal.timeout(10_000) to fetch in setup auth-provider/test endpoint - Add .replace(/\/$/, "") to strip trailing slash from internalBaseUrl - Delete dead routes/admin/authProvider.ts Co-Authored-By: Paperclip --- apps/api/src/routes/admin/authProvider.ts | 195 ---------------------- apps/api/src/routes/authProvider.ts | 4 + apps/api/src/routes/setup.ts | 4 +- 3 files changed, 6 insertions(+), 197 deletions(-) delete mode 100644 apps/api/src/routes/admin/authProvider.ts diff --git a/apps/api/src/routes/admin/authProvider.ts b/apps/api/src/routes/admin/authProvider.ts deleted file mode 100644 index faeb536..0000000 --- a/apps/api/src/routes/admin/authProvider.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Hono } from "hono"; -import { zValidator } from "@hono/zod-validator"; -import { z } from "zod/v3"; -import { eq, getDb, authProviderConfig, encryptSecret } from "@groombook/db"; -import { requireSuperUser } from "../../middleware/rbac.js"; -import { reinitAuth } from "../../lib/auth.js"; - -export const authProviderRouter = new Hono(); - -// ─── Schemas ───────────────────────────────────────────────────────────────── - -const putAuthProviderSchema = z.object({ - providerId: z.string().min(1).max(100), - displayName: z.string().min(1).max(200), - issuerUrl: z.string().url(), - internalBaseUrl: z.string().url().nullable().optional(), - clientId: z.string().min(1), - clientSecret: z.string().min(1), - scopes: z.string().default("openid profile email"), -}); - -// ─── GET /api/admin/auth-provider ──────────────────────────────────────────── - -authProviderRouter.get("/", requireSuperUser(), async (c) => { - const db = getDb(); - const [row] = await db - .select() - .from(authProviderConfig) - .where(eq(authProviderConfig.enabled, true)) - .limit(1); - - if (!row) { - return c.json({ exists: false, config: null }); - } - - // Return config with secret redacted - return c.json({ - exists: true, - config: { - id: row.id, - providerId: row.providerId, - displayName: row.displayName, - issuerUrl: row.issuerUrl, - internalBaseUrl: row.internalBaseUrl, - clientId: row.clientId, - clientSecret: "••••••••", - scopes: row.scopes, - enabled: row.enabled, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - }, - }); -}); - -// ─── PUT /api/admin/auth-provider ─────────────────────────────────────────── - -authProviderRouter.put( - "/", - requireSuperUser(), - zValidator("json", putAuthProviderSchema), - async (c) => { - const db = getDb(); - const body = c.req.valid("json"); - - // Encrypt the client secret before storing - const encryptedSecret = encryptSecret(body.clientSecret); - - // Check if config already exists - const [existing] = await db - .select({ id: authProviderConfig.id }) - .from(authProviderConfig) - .where(eq(authProviderConfig.providerId, body.providerId)) - .limit(1); - - let saved; - if (existing) { - // Update existing - [saved] = await db - .update(authProviderConfig) - .set({ - displayName: body.displayName, - issuerUrl: body.issuerUrl, - internalBaseUrl: body.internalBaseUrl ?? null, - clientId: body.clientId, - clientSecret: encryptedSecret, - scopes: body.scopes, - updatedAt: new Date(), - }) - .where(eq(authProviderConfig.id, existing.id)) - .returning(); - } else { - // Insert new - [saved] = await db - .insert(authProviderConfig) - .values({ - providerId: body.providerId, - displayName: body.displayName, - issuerUrl: body.issuerUrl, - internalBaseUrl: body.internalBaseUrl ?? null, - clientId: body.clientId, - clientSecret: encryptedSecret, - scopes: body.scopes, - enabled: true, - }) - .returning(); - } - - await reinitAuth(); - - // Return config with secret redacted - return c.json({ - id: saved!.id, - providerId: saved!.providerId, - displayName: saved!.displayName, - issuerUrl: saved!.issuerUrl, - internalBaseUrl: saved!.internalBaseUrl, - clientId: saved!.clientId, - clientSecret: "••••••••", - scopes: saved!.scopes, - enabled: saved!.enabled, - createdAt: saved!.createdAt, - updatedAt: saved!.updatedAt, - }); - } -); - -// ─── POST /api/admin/auth-provider/test ───────────────────────────────────── - -const testAuthProviderSchema = z.object({ - issuerUrl: z.string().url(), - internalBaseUrl: z.string().url().nullable().optional(), -}); - -authProviderRouter.post( - "/test", - requireSuperUser(), - zValidator("json", testAuthProviderSchema), - async (c) => { - const { issuerUrl, internalBaseUrl } = c.req.valid("json"); - - // Fetch OIDC discovery document - const discoveryUrl = internalBaseUrl - ? `${internalBaseUrl.replace(/\/$/, "")}/application/o/.well-known/openid-configuration` - : `${issuerUrl.replace(/\/$/, "")}/.well-known/openid-configuration`; - - let metadata: Record | null = null; - let errorMessage: string | null = null; - - try { - const response = await fetch(discoveryUrl, { - method: "GET", - headers: { "Accept": "application/json" }, - signal: AbortSignal.timeout(10_000), - }); - - if (!response.ok) { - errorMessage = `HTTP ${response.status}: ${response.statusText}`; - } else { - metadata = (await response.json()) as Record; - } - } catch (err) { - errorMessage = - err instanceof Error ? err.message : "Failed to fetch OIDC discovery document"; - } - - if (errorMessage) { - return c.json({ ok: false, error: errorMessage }); - } - - return c.json({ ok: true, metadata }); - } -); - -// ─── DELETE /api/admin/auth-provider ──────────────────────────────────────── - -authProviderRouter.delete("/", requireSuperUser(), async (c) => { - const db = getDb(); - - // Get the current config - const [existing] = await db - .select({ id: authProviderConfig.id }) - .from(authProviderConfig) - .where(eq(authProviderConfig.enabled, true)) - .limit(1); - - if (!existing) { - return c.json({ ok: true, message: "No DB config to delete" }); - } - - await db.delete(authProviderConfig).where(eq(authProviderConfig.id, existing.id)); - - await reinitAuth(); - - return c.json({ ok: true, message: "Auth provider config removed; auth will fall back to env vars" }); -}); diff --git a/apps/api/src/routes/authProvider.ts b/apps/api/src/routes/authProvider.ts index 40ea527..bbf8dc4 100644 --- a/apps/api/src/routes/authProvider.ts +++ b/apps/api/src/routes/authProvider.ts @@ -3,6 +3,7 @@ import { zValidator } from "@hono/zod-validator"; import { z } from "zod/v3"; import { eq, getDb, authProviderConfig, encryptSecret } from "@groombook/db"; import { requireSuperUser } from "../middleware/rbac.js"; +import { reinitAuth } from "../lib/auth.js"; export const authProviderRouter = new Hono(); @@ -87,6 +88,8 @@ authProviderRouter.put( if (!row) return c.json({ error: "Failed to create auth provider config" }, 500); + await reinitAuth(); + return c.json({ id: row.id, providerId: row.providerId, @@ -142,6 +145,7 @@ authProviderRouter.delete( async (c) => { const db = getDb(); await db.delete(authProviderConfig); + await reinitAuth(); return c.json({ ok: true }); } ); diff --git a/apps/api/src/routes/setup.ts b/apps/api/src/routes/setup.ts index 37bb3a2..2b94d20 100644 --- a/apps/api/src/routes/setup.ts +++ b/apps/api/src/routes/setup.ts @@ -210,11 +210,11 @@ setupRouter.post("/auth-provider/test", async (c) => { // Determine the discovery URL const discoveryUrl = body.internalBaseUrl - ? `${body.internalBaseUrl}/application/o/.well-known/openid-configuration` + ? `${body.internalBaseUrl.replace(/\/$/, "")}/application/o/.well-known/openid-configuration` : `${body.issuerUrl}/.well-known/openid-configuration`; try { - const res = await fetch(discoveryUrl, { method: "GET" }); + const res = await fetch(discoveryUrl, { method: "GET", signal: AbortSignal.timeout(10_000) }); if (!res.ok) { return c.json({ ok: false,