Files
stack/apps/api/src/auth/auth.config.ts
Jason Woltje 7e983e2455 fix(#337): Validate OIDC configuration at startup, fail fast if missing
- Add OIDC_ENABLED environment variable to control OIDC authentication
- Validate required OIDC env vars (OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET)
  are present when OIDC is enabled
- Validate OIDC_ISSUER ends with trailing slash for correct discovery URL
- Throw descriptive error at startup if configuration is invalid
- Skip OIDC plugin registration when OIDC is disabled
- Add comprehensive tests for validation logic (17 test cases)

Refs #337

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 15:39:47 -06:00

107 lines
3.2 KiB
TypeScript

import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { genericOAuth } from "better-auth/plugins";
import type { PrismaClient } from "@prisma/client";
/**
* Required OIDC environment variables when OIDC is enabled
*/
const REQUIRED_OIDC_ENV_VARS = ["OIDC_ISSUER", "OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET"] as const;
/**
* Check if OIDC authentication is enabled via environment variable
*/
export function isOidcEnabled(): boolean {
const enabled = process.env.OIDC_ENABLED;
return enabled === "true" || enabled === "1";
}
/**
* Validates OIDC configuration at startup.
* Throws an error if OIDC is enabled but required environment variables are missing.
*
* @throws Error if OIDC is enabled but required vars are missing or empty
*/
export function validateOidcConfig(): void {
if (!isOidcEnabled()) {
// OIDC is disabled, no validation needed
return;
}
const missingVars: string[] = [];
for (const envVar of REQUIRED_OIDC_ENV_VARS) {
const value = process.env[envVar];
if (!value || value.trim() === "") {
missingVars.push(envVar);
}
}
if (missingVars.length > 0) {
throw new Error(
`OIDC authentication is enabled (OIDC_ENABLED=true) but required environment variables are missing or empty: ${missingVars.join(", ")}. ` +
`Either set these variables or disable OIDC by setting OIDC_ENABLED=false.`
);
}
// Additional validation: OIDC_ISSUER should end with a trailing slash for proper discovery URL
const issuer = process.env.OIDC_ISSUER;
if (issuer && !issuer.endsWith("/")) {
throw new Error(
`OIDC_ISSUER must end with a trailing slash (/). Current value: "${issuer}". ` +
`The discovery URL is constructed by appending ".well-known/openid-configuration" to the issuer.`
);
}
}
/**
* Get OIDC plugins configuration.
* Returns empty array if OIDC is disabled, otherwise returns configured OAuth plugin.
*/
function getOidcPlugins(): ReturnType<typeof genericOAuth>[] {
if (!isOidcEnabled()) {
return [];
}
return [
genericOAuth({
config: [
{
providerId: "authentik",
clientId: process.env.OIDC_CLIENT_ID ?? "",
clientSecret: process.env.OIDC_CLIENT_SECRET ?? "",
discoveryUrl: `${process.env.OIDC_ISSUER ?? ""}.well-known/openid-configuration`,
scopes: ["openid", "profile", "email"],
},
],
}),
];
}
export function createAuth(prisma: PrismaClient) {
// Validate OIDC configuration at startup - fail fast if misconfigured
validateOidcConfig();
return betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true, // Enable for now, can be disabled later
},
plugins: [...getOidcPlugins()],
session: {
expiresIn: 60 * 60 * 24, // 24 hours
updateAge: 60 * 60 * 24, // 24 hours
},
trustedOrigins: [
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
"http://localhost:3001", // API origin (dev)
"https://app.mosaicstack.dev", // Production web
"https://api.mosaicstack.dev", // Production API
],
});
}
export type Auth = ReturnType<typeof createAuth>;