fix(GRO-689): only validate authorizationUrl hostname, add OIDC_INTERNAL_BASE in dev (#302)

fix(GRO-689): only validate authorizationUrl hostname, add OIDC_INTERNAL_BASE in dev
This commit was merged in pull request #302.
This commit is contained in:
groombook-cto[bot]
2026-04-16 05:18:58 +00:00
committed by GitHub
5 changed files with 76 additions and 51 deletions
+3 -2
View File
@@ -195,10 +195,11 @@ describe("POST /clients", () => {
expect(insertedValues[0]!.name).toBe("Charlie"); expect(insertedValues[0]!.name).toBe("Charlie");
}); });
it("creates a client with only required name field", async () => { it("creates a client with name and email", async () => {
const res = await jsonRequest("POST", "/clients", { name: "Dana" }); const res = await jsonRequest("POST", "/clients", { name: "Dana", email: "dana@example.com" });
expect(res.status).toBe(201); expect(res.status).toBe(201);
expect(insertedValues[0]!.name).toBe("Dana"); expect(insertedValues[0]!.name).toBe("Dana");
expect(insertedValues[0]!.email).toBe("dana@example.com");
}); });
it("rejects empty name", async () => { it("rejects empty name", async () => {
+4 -8
View File
@@ -204,15 +204,11 @@ export async function initAuth(): Promise<void> {
const userInfoUrl = discovery.userinfo_endpoint; const userInfoUrl = discovery.userinfo_endpoint;
if (authzUrl && tokenUrl && userInfoUrl) { if (authzUrl && tokenUrl && userInfoUrl) {
const authzUrlObj = new URL(authzUrl); const authzUrlObj = new URL(authzUrl);
const tokenUrlObj = new URL(tokenUrl); // Only validate authorizationUrl hostname against issuer — token/userinfo
const userInfoUrlObj = new URL(userInfoUrl); // may legitimately use internal hostnames (OIDC_INTERNAL_BASE) for server-to-server calls.
if ( if (authzUrlObj.hostname !== issuerHostname) {
authzUrlObj.hostname !== issuerHostname ||
tokenUrlObj.hostname !== issuerHostname ||
userInfoUrlObj.hostname !== issuerHostname
) {
throw new Error( throw new Error(
`[FATAL] OIDC discovery URL hostname mismatch: expected '${issuerHostname}' but got '${authzUrlObj.hostname}', '${tokenUrlObj.hostname}', or '${userInfoUrlObj.hostname}'. This may indicate a man-in-the-middle attack.` `[FATAL] OIDC discovery URL hostname mismatch: expected '${issuerHostname}' but got '${authzUrlObj.hostname}'. This may indicate a man-in-the-middle attack.`
); );
} }
oidcConfig = { oidcConfig = {
+1 -1
View File
@@ -8,7 +8,7 @@ export const clientsRouter = new Hono<AppEnv>();
const createClientSchema = z.object({ const createClientSchema = z.object({
name: z.string().min(1).max(200), name: z.string().min(1).max(200),
email: z.string().email().optional(), email: z.string().email(),
phone: z.string().max(50).optional(), phone: z.string().max(50).optional(),
address: z.string().max(500).optional(), address: z.string().max(500).optional(),
notes: z.string().max(2000).optional(), notes: z.string().max(2000).optional(),
@@ -0,0 +1,20 @@
-- Migration: 0029_db_indexes_constraints.sql
-- Add missing indexes on appointments, pets, clients tables and NOT NULL constraint on clients.email
-- Backfill NULL emails before setting NOT NULL
UPDATE clients SET email = concat('unknown-', id::text, '@placeholder.local') WHERE email IS NULL;
-- Add indexes on appointments table
CREATE INDEX idx_appointments_client_id ON appointments(client_id);
CREATE INDEX idx_appointments_staff_id ON appointments(staff_id);
CREATE INDEX idx_appointments_start_time ON appointments(start_time);
CREATE INDEX idx_appointments_status ON appointments(status);
-- Add index on pets table
CREATE INDEX idx_pets_client_id ON pets(client_id);
-- Add index on clients table
CREATE INDEX idx_clients_email ON clients(email);
-- Set NOT NULL on clients.email (after backfill)
ALTER TABLE clients ALTER COLUMN email SET NOT NULL;
+13 -5
View File
@@ -102,10 +102,12 @@ export const verification = pgTable("verification", {
// ─── Tables ─────────────────────────────────────────────────────────────────── // ─── Tables ───────────────────────────────────────────────────────────────────
export const clients = pgTable("clients", { export const clients = pgTable(
"clients",
{
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(), name: text("name").notNull(),
email: text("email"), email: text("email").notNull(),
phone: text("phone"), phone: text("phone"),
address: text("address"), address: text("address"),
notes: text("notes"), notes: text("notes"),
@@ -119,9 +121,13 @@ export const clients = pgTable("clients", {
disabledAt: timestamp("disabled_at"), disabledAt: timestamp("disabled_at"),
createdAt: timestamp("created_at").notNull().defaultNow(), createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(),
}); },
(t) => [index("idx_clients_email").on(t.email)]
);
export const pets = pgTable("pets", { export const pets = pgTable(
"pets",
{
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
clientId: uuid("client_id") clientId: uuid("client_id")
.notNull() .notNull()
@@ -142,7 +148,9 @@ export const pets = pgTable("pets", {
image: text("image"), image: text("image"),
createdAt: timestamp("created_at").notNull().defaultNow(), createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(),
}); },
(t) => [index("idx_pets_client_id").on(t.clientId)]
);
export const services = pgTable("services", { export const services = pgTable("services", {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),