diff --git a/.env.example b/.env.example index d0c791c..7ab85b4 100644 --- a/.env.example +++ b/.env.example @@ -20,8 +20,9 @@ SUDLOH_OIDC_ISSUER=https://account.sudloh.com/api/auth SUDLOH_OIDC_CLIENT_ID= SUDLOH_OIDC_CLIENT_SECRET= SUDLOH_OIDC_REDIRECT_URI= -# Set after linking existing Guide accounts; this also enables Sudloh session checks. +# Set after linking existing Guide accounts to require Sudloh sign-in. SUDLOH_OIDC_ONLY=false +SUDLOH_OIDC_PAUSED=false # Resend sending key; verify sudloh.com before sending from no-reply@sudloh.com. RESEND_API_KEY=replace-with-resend-sending-key diff --git a/README.md b/README.md index 15a3a15..e681fde 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,13 @@ bun run worker:outbox bun run worker:discord ``` +For a temporary return to Guide email/password login, set +`SUDLOH_OIDC_ONLY=false` and `SUDLOH_OIDC_PAUSED=true` in the deployment +ConfigMap. The Sudloh client credentials can stay in the Secret. Existing Guide +sessions remain valid. Users who joined only through Sudloh can use Guide's +password-reset page to create a local password. Restore OIDC by setting +`SUDLOH_OIDC_PAUSED=false` and `SUDLOH_OIDC_ONLY=true`. + Email, commission payments, and push notifications need their corresponding service credentials. See [External prerequisites](#external-prerequisites) for the production configuration details. diff --git a/k8s/base/configmap.yaml b/k8s/base/configmap.yaml index facd8b5..503ad1b 100644 --- a/k8s/base/configmap.yaml +++ b/k8s/base/configmap.yaml @@ -9,7 +9,8 @@ data: BETTER_AUTH_URL: https://guide.sudloh.com SUDLOH_OIDC_ISSUER: https://account.sudloh.com/api/auth SUDLOH_OIDC_REDIRECT_URI: https://guide.sudloh.com/api/auth/callback/sudloh - SUDLOH_OIDC_ONLY: "true" + SUDLOH_OIDC_ONLY: "false" + SUDLOH_OIDC_PAUSED: "true" # Traefik supplies X-Forwarded-For and does not trust incoming forwarded headers. # The network policy allows only Traefik to reach the app. TRUSTED_CLIENT_IP_HEADER: x-forwarded-for diff --git a/lib/auth/server.test.ts b/lib/auth/server.test.ts index bf864dc..4a33120 100644 --- a/lib/auth/server.test.ts +++ b/lib/auth/server.test.ts @@ -11,7 +11,7 @@ vi.mock("better-auth/plugins", () => ({ admin: (options: unknown) => options, ca genericOAuth: (options: unknown) => ({ id: "generic-oauth", options }), emailOTP: (options: unknown) => ({ id: "email-otp", options }) })); -const envNames = ["NODE_ENV", "DATABASE_URL", "BETTER_AUTH_URL", "BETTER_AUTH_SECRET", "BUZZ_DEMO_MODE", "RESEND_API_KEY", "SUDLOH_OIDC_CLIENT_ID", "SUDLOH_OIDC_CLIENT_SECRET", "SUDLOH_OIDC_REDIRECT_URI", "SUDLOH_OIDC_ISSUER", "SUDLOH_OIDC_ONLY", "TRUSTED_CLIENT_IP_HEADER"] as const; +const envNames = ["NODE_ENV", "DATABASE_URL", "BETTER_AUTH_URL", "BETTER_AUTH_SECRET", "BUZZ_DEMO_MODE", "RESEND_API_KEY", "SUDLOH_OIDC_CLIENT_ID", "SUDLOH_OIDC_CLIENT_SECRET", "SUDLOH_OIDC_REDIRECT_URI", "SUDLOH_OIDC_ISSUER", "SUDLOH_OIDC_ONLY", "SUDLOH_OIDC_PAUSED", "TRUSTED_CLIENT_IP_HEADER"] as const; const testEnv = process.env as Record; const originalEnv = envNames.map((name) => testEnv[name]); @@ -28,6 +28,7 @@ beforeEach(() => { delete testEnv.SUDLOH_OIDC_REDIRECT_URI; delete testEnv.SUDLOH_OIDC_ISSUER; delete testEnv.SUDLOH_OIDC_ONLY; + delete testEnv.SUDLOH_OIDC_PAUSED; delete testEnv.TRUSTED_CLIENT_IP_HEADER; }); afterEach(() => { @@ -83,6 +84,18 @@ describe("actual administrator session boundary", () => { redirectURI: "https://guide.example.test/api/auth/callback/sudloh", }); }); + it("uses local sign-in while Sudloh is paused, even with client credentials present", async () => { + testEnv.SUDLOH_OIDC_CLIENT_ID = "client"; + testEnv.SUDLOH_OIDC_CLIENT_SECRET = "secret"; + testEnv.SUDLOH_OIDC_REDIRECT_URI = "https://guide.example.test/api/auth/callback/sudloh"; + testEnv.SUDLOH_OIDC_PAUSED = "true"; + const { getAuth, isSudlohOidcEnabled } = await import("./server"); + expect(isSudlohOidcEnabled()).toBe(false); + getAuth(); + const options = mocks.auth.mock.calls.at(-1)![0]; + expect(options.emailAndPassword).toMatchObject({ enabled: true, disableSignUp: false }); + expect(options.plugins.some((plugin: { id?: string }) => plugin.id === "generic-oauth")).toBe(false); + }); it("disables local sign-in after the OIDC cutover flag is set", async () => { testEnv.SUDLOH_OIDC_CLIENT_ID = "client"; testEnv.SUDLOH_OIDC_CLIENT_SECRET = "secret"; diff --git a/lib/auth/server.ts b/lib/auth/server.ts index 3fd1720..69cb8c4 100644 --- a/lib/auth/server.ts +++ b/lib/auth/server.ts @@ -33,6 +33,7 @@ function hasAuthConfiguration(): boolean { } export function isSudlohOidcEnabled(): boolean { + if (process.env.SUDLOH_OIDC_PAUSED === "true") return false; const clientId = process.env.SUDLOH_OIDC_CLIENT_ID; const clientSecret = process.env.SUDLOH_OIDC_CLIENT_SECRET; if (!clientId && !clientSecret) return false;