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>
This commit is contained in:
Jason Woltje
2026-02-05 15:39:47 -06:00
parent e237c40482
commit 7e983e2455
3 changed files with 223 additions and 14 deletions

View File

@@ -0,0 +1,138 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { isOidcEnabled, validateOidcConfig } from "./auth.config";
describe("auth.config", () => {
// Store original env vars to restore after each test
const originalEnv = { ...process.env };
beforeEach(() => {
// Clear relevant env vars before each test
delete process.env.OIDC_ENABLED;
delete process.env.OIDC_ISSUER;
delete process.env.OIDC_CLIENT_ID;
delete process.env.OIDC_CLIENT_SECRET;
});
afterEach(() => {
// Restore original env vars
process.env = { ...originalEnv };
});
describe("isOidcEnabled", () => {
it("should return false when OIDC_ENABLED is not set", () => {
expect(isOidcEnabled()).toBe(false);
});
it("should return false when OIDC_ENABLED is 'false'", () => {
process.env.OIDC_ENABLED = "false";
expect(isOidcEnabled()).toBe(false);
});
it("should return false when OIDC_ENABLED is '0'", () => {
process.env.OIDC_ENABLED = "0";
expect(isOidcEnabled()).toBe(false);
});
it("should return false when OIDC_ENABLED is empty string", () => {
process.env.OIDC_ENABLED = "";
expect(isOidcEnabled()).toBe(false);
});
it("should return true when OIDC_ENABLED is 'true'", () => {
process.env.OIDC_ENABLED = "true";
expect(isOidcEnabled()).toBe(true);
});
it("should return true when OIDC_ENABLED is '1'", () => {
process.env.OIDC_ENABLED = "1";
expect(isOidcEnabled()).toBe(true);
});
});
describe("validateOidcConfig", () => {
describe("when OIDC is disabled", () => {
it("should not throw when OIDC_ENABLED is not set", () => {
expect(() => validateOidcConfig()).not.toThrow();
});
it("should not throw when OIDC_ENABLED is false even if vars are missing", () => {
process.env.OIDC_ENABLED = "false";
// Intentionally not setting any OIDC vars
expect(() => validateOidcConfig()).not.toThrow();
});
});
describe("when OIDC is enabled", () => {
beforeEach(() => {
process.env.OIDC_ENABLED = "true";
});
it("should throw when OIDC_ISSUER is missing", () => {
process.env.OIDC_CLIENT_ID = "test-client-id";
process.env.OIDC_CLIENT_SECRET = "test-client-secret";
expect(() => validateOidcConfig()).toThrow("OIDC_ISSUER");
expect(() => validateOidcConfig()).toThrow("OIDC authentication is enabled");
});
it("should throw when OIDC_CLIENT_ID is missing", () => {
process.env.OIDC_ISSUER = "https://auth.example.com/";
process.env.OIDC_CLIENT_SECRET = "test-client-secret";
expect(() => validateOidcConfig()).toThrow("OIDC_CLIENT_ID");
});
it("should throw when OIDC_CLIENT_SECRET is missing", () => {
process.env.OIDC_ISSUER = "https://auth.example.com/";
process.env.OIDC_CLIENT_ID = "test-client-id";
expect(() => validateOidcConfig()).toThrow("OIDC_CLIENT_SECRET");
});
it("should throw when all required vars are missing", () => {
expect(() => validateOidcConfig()).toThrow(
"OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET"
);
});
it("should throw when vars are empty strings", () => {
process.env.OIDC_ISSUER = "";
process.env.OIDC_CLIENT_ID = "";
process.env.OIDC_CLIENT_SECRET = "";
expect(() => validateOidcConfig()).toThrow(
"OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET"
);
});
it("should throw when vars are whitespace only", () => {
process.env.OIDC_ISSUER = " ";
process.env.OIDC_CLIENT_ID = "test-client-id";
process.env.OIDC_CLIENT_SECRET = "test-client-secret";
expect(() => validateOidcConfig()).toThrow("OIDC_ISSUER");
});
it("should throw when OIDC_ISSUER does not end with trailing slash", () => {
process.env.OIDC_ISSUER = "https://auth.example.com/application/o/mosaic";
process.env.OIDC_CLIENT_ID = "test-client-id";
process.env.OIDC_CLIENT_SECRET = "test-client-secret";
expect(() => validateOidcConfig()).toThrow("OIDC_ISSUER must end with a trailing slash");
expect(() => validateOidcConfig()).toThrow("https://auth.example.com/application/o/mosaic");
});
it("should not throw with valid complete configuration", () => {
process.env.OIDC_ISSUER = "https://auth.example.com/application/o/mosaic-stack/";
process.env.OIDC_CLIENT_ID = "test-client-id";
process.env.OIDC_CLIENT_SECRET = "test-client-secret";
expect(() => validateOidcConfig()).not.toThrow();
});
it("should suggest disabling OIDC in error message", () => {
expect(() => validateOidcConfig()).toThrow("OIDC_ENABLED=false");
});
});
});
});