From 915877b8e64d66877098ca48f9f032a80361f4f9 Mon Sep 17 00:00:00 2001 From: Patrick McEvoy Date: Tue, 2 Dec 2025 00:51:08 +0500 Subject: [PATCH] feat: limit oidc domains (#263) --- .env.example | 3 ++- README.md | 1 + cloud/docker-compose.yml | 1 + docker-compose.yml | 1 + packages/auth/src/auth.ts | 32 +++++++++++++++++++++++++++++++- turbo.json | 1 + 6 files changed, 37 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index c1c35b60..2616a5f8 100644 --- a/.env.example +++ b/.env.example @@ -43,6 +43,8 @@ TRELLO_APP_SECRET= # OAuth providers (optional) BETTER_AUTH_TRUSTED_ORIGINS= +# Optional: Restrict OIDC/Social sign-ins to specific email domains (comma-separated) +BETTER_AUTH_ALLOWED_DOMAINS= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= DISCORD_CLIENT_ID= @@ -80,4 +82,3 @@ TWITCH_CLIENT_SECRET= APPLE_CLIENT_ID= APPLE_CLIENT_SECRET= APPLE_APP_BUNDLE_IDENTIFIER= - diff --git a/README.md b/README.md index 55e48c40..a43c422c 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ pnpm dev | `SMTP_REJECT_UNAUTHORIZED` | Reject invalid certificates (defaults to true if not set) | For Email | `false` | | `NEXT_PUBLIC_DISABLE_EMAIL` | To disable all email features | For Email | `true` | | `NEXT_PUBLIC_BASE_URL` | Base URL of your installation | Yes | `http://localhost:3000` | +| `BETTER_AUTH_ALLOWED_DOMAINS` | Comma-separated list of allowed domains for OIDC logins | For OIDC/Social login | `example.com,subsidiary.com` | | `BETTER_AUTH_SECRET` | Auth encryption secret | Yes | Random 32+ char string | | `BETTER_AUTH_TRUSTED_ORIGINS` | Allowed callback origins | No | `http://localhost:3000,http://localhost:3001` | | `GOOGLE_CLIENT_ID` | Google OAuth client ID | For Google login | `xxx.apps.googleusercontent.com` | diff --git a/cloud/docker-compose.yml b/cloud/docker-compose.yml index d3bb4418..78bab191 100644 --- a/cloud/docker-compose.yml +++ b/cloud/docker-compose.yml @@ -65,6 +65,7 @@ services: - BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + - BETTER_AUTH_ALLOWED_DOMAINS=${BETTER_AUTH_ALLOWED_DOMAINS} # Analytics - NEXT_PUBLIC_UMAMI_ID=${NEXT_PUBLIC_UMAMI_ID} diff --git a/docker-compose.yml b/docker-compose.yml index 0ccd9b5e..1e1fc06a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,6 +60,7 @@ services: - BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + - BETTER_AUTH_ALLOWED_DOMAINS=${BETTER_AUTH_ALLOWED_DOMAINS} - DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID} - DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET} - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index c6b737eb..497cf614 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -32,6 +32,9 @@ export const configuredProviders = socialProviderList.reduce< requireSelectAccount?: boolean; clientKey?: string; issuer?: string; + // Google-specific optional hints + hostedDomain?: string; + hd?: string; } > >((acc, provider) => { @@ -69,6 +72,22 @@ export const configuredProviders = socialProviderList.reduce< acc[provider].tenantId = "common"; acc[provider].requireSelectAccount = true; } + // Add Google domain hint if allowed domains is configured + if ( + provider === "google" && + Object.keys(acc).includes("google") && + acc[provider] + ) { + const allowed = process.env.BETTER_AUTH_ALLOWED_DOMAINS + ?.split(",") + .map((d) => d.trim().toLowerCase()) + .filter(Boolean); + if (allowed && allowed.length > 0) { + // Use the first domain as an authorization hint + acc[provider].hostedDomain = allowed[0]; + acc[provider].hd = allowed[0]; + } + } if ( provider === "tiktok" && Object.keys(acc).includes("tiktok") && @@ -343,7 +362,18 @@ export const initAuth = (db: dbClient) => { return Promise.resolve(false); } - return Promise.resolve(true); + // Fall through to any additional checks below + } + // Enforce allowed domains (OIDC/social) if configured + const allowed = process.env.BETTER_AUTH_ALLOWED_DOMAINS + ?.split(",") + .map((d) => d.trim().toLowerCase()) + .filter(Boolean); + if (allowed && allowed.length > 0) { + const domain = user.email.split("@")[1]?.toLowerCase(); + if (!domain || !allowed.includes(domain)) { + return Promise.resolve(false); + } } return Promise.resolve(true); }, diff --git a/turbo.json b/turbo.json index eb3c1fdc..22b8c9be 100644 --- a/turbo.json +++ b/turbo.json @@ -51,6 +51,7 @@ "TRELLO_APP_SECRET", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", + "BETTER_AUTH_ALLOWED_DOMAINS", "DISCORD_CLIENT_ID", "DISCORD_CLIENT_SECRET", "GITHUB_CLIENT_ID",