From 562d78e2d7f20db0f1d5dc63375379b3af0d07c5 Mon Sep 17 00:00:00 2001 From: Kendry Grullon Date: Tue, 30 Jun 2026 02:08:09 -0400 Subject: [PATCH] feat: add granular signin disable flags and OIDC auto-redirect (#2857) --- .env.example | 14 +++ .../configuration/environment.mdx | 54 +++++++++ .../deployment/docker-compose.mdx | 13 ++ .../docs/self-hosting/deployment/docker.mdx | 6 + .../docs/self-hosting/deployment/railway.mdx | 6 + apps/remix/app/components/forms/signin.tsx | 112 ++++++++++-------- .../_unauthenticated+/forgot-password.tsx | 11 +- .../reset-password.$token.tsx | 5 + .../reset-password._index.tsx | 11 +- .../app/routes/_unauthenticated+/signin.tsx | 56 ++++++++- docker/production/compose.yml | 6 + .../auth/server/lib/errors/error-codes.ts | 1 + packages/auth/server/routes/email-password.ts | 25 ++++ packages/lib/constants/auth.ts | 27 +++++ packages/lib/translations/de/web.po | 5 + packages/lib/translations/en/web.po | 5 + packages/lib/translations/es/web.po | 5 + packages/lib/translations/fr/web.po | 5 + packages/lib/translations/it/web.po | 5 + packages/lib/translations/ja/web.po | 5 + packages/lib/translations/ko/web.po | 5 + packages/lib/translations/nl/web.po | 5 + packages/lib/translations/pl/web.po | 5 + packages/lib/translations/pt-BR/web.po | 5 + packages/lib/translations/zh/web.po | 5 + packages/tsconfig/process-env.d.ts | 7 ++ render.yaml | 12 ++ turbo.json | 6 + 28 files changed, 371 insertions(+), 56 deletions(-) diff --git a/.env.example b/.env.example index be3ecaab3..5f3da7c1f 100644 --- a/.env.example +++ b/.env.example @@ -180,6 +180,20 @@ NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP= NEXT_PUBLIC_DISABLE_OIDC_SIGNUP= # OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org). NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS= +# OPTIONAL: Set to "true" to disable all signin methods (email, Google, Microsoft, OIDC). +NEXT_PUBLIC_DISABLE_SIGNIN= +# OPTIONAL: Set to "true" to disable email/password signin only. Also closes /forgot-password and /reset-password. +NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN= +# OPTIONAL: Set to "true" to hide the Google signin button. +NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN= +# OPTIONAL: Set to "true" to hide the Microsoft signin button. +NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN= +# OPTIONAL: Set to "true" to hide the OIDC signin button. +NEXT_PUBLIC_DISABLE_OIDC_SIGNIN= +# OPTIONAL: When OIDC is the only enabled signin transport, /signin auto-redirects +# to the OIDC provider (rendering only a spinner). Set to "true" to disable this +# and keep showing the signin page. +NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT= # OPTIONAL: Set to true to use internal webapp url in browserless requests. NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false diff --git a/apps/docs/content/docs/self-hosting/configuration/environment.mdx b/apps/docs/content/docs/self-hosting/configuration/environment.mdx index 4ec25e213..4b910b43b 100644 --- a/apps/docs/content/docs/self-hosting/configuration/environment.mdx +++ b/apps/docs/content/docs/self-hosting/configuration/environment.mdx @@ -272,6 +272,12 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft. Existing linked users can still sign in | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC, including the organisation portal | `false` | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | | +| `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch. Disable all signin methods application-wide | `false` | +| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin. Also closes `/forgot-password` and `/reset-password` | `false` | +| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` | +| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable the automatic `/signin` redirect when OIDC is the only enabled transport | `false` | | `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | | | `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` | @@ -303,6 +309,44 @@ NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true" NEXT_PUBLIC_DISABLE_SIGNUP="true" ``` +### Sign-in Restrictions + +You can control which methods are available for users to sign in with the following environment variables: + +- **`NEXT_PUBLIC_DISABLE_SIGNIN`** (master switch): Set to `true` to block all signin methods (email/password, Google, Microsoft, OIDC). Hides every signin entry point on `/signin` and rejects email/password signin server-side with a `SIGNIN_DISABLED` error. +- **`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN`**: Set to `true` to disable email/password signin only. The email/password form is hidden, the `/forgot-password` and `/reset-password` pages redirect to `/signin`, and the corresponding server endpoints reject requests. SSO signin is unaffected. +- **`NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_OIDC_SIGNIN`**: Set to `true` to hide the matching SSO button on the signin page. Useful when an SSO provider is kept configured for account linking but not advertised as a signin entry point. + +These flags are opt-in: when none are set, signin behaviour is unchanged from a stock Documenso instance. + +```bash +# Allow only OIDC signin (e.g. enterprise SSO-only) +NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true" +NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true" +NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true" + +# Or disable signin entirely +NEXT_PUBLIC_DISABLE_SIGNIN="true" +``` + +### OIDC Auto-redirect + +When OIDC is the only enabled signin transport on your instance, `/signin` automatically redirects users straight to the OIDC provider instead of showing the signin form. The page renders a spinner while the redirect happens. No extra configuration is required — disabling every other signin method is enough to trigger it. + +- **`NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT`**: Set to `true` to opt out of the automatic redirect and keep rendering the signin page even when OIDC is the only enabled transport. + +The redirect only triggers when OIDC is configured and email/password, Google, and Microsoft signin are all disabled. If any other transport remains enabled, the signin form is shown as normal. + +```bash +# OIDC-only signin: disabling all other methods auto-redirects to the provider +NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true" +NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true" +NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true" + +# Opt out of the auto-redirect while still OIDC-only +# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true" +``` + --- ## AI Features @@ -446,6 +490,16 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" # NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true" # NEXT_PUBLIC_DISABLE_OIDC_SIGNUP="true" # NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org" + +# Sign-in restrictions (optional) +# NEXT_PUBLIC_DISABLE_SIGNIN="true" +# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true" +# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true" +# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true" +# NEXT_PUBLIC_DISABLE_OIDC_SIGNIN="true" + +# Opt out of the automatic OIDC redirect when OIDC is the only enabled transport (optional) +# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true" ``` --- diff --git a/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx b/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx index b3590a7f2..84e228115 100644 --- a/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx @@ -163,6 +163,19 @@ NEXT_PUBLIC_DISABLE_SIGNUP=false # NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true # NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true # NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org + +# Signin restrictions (optional) +# Master switch — disables every signin method +# NEXT_PUBLIC_DISABLE_SIGNIN=true +# Per-method switches (optional). Each disables that signin path. +# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN=true +# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN=true +# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN=true +# NEXT_PUBLIC_DISABLE_OIDC_SIGNIN=true + +# When OIDC is the only enabled transport, /signin auto-redirects to the provider. +# Set this to opt out and keep showing the signin page (optional). +# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=true ``` Generate secure secrets using: `openssl rand -base64 32` diff --git a/apps/docs/content/docs/self-hosting/deployment/docker.mdx b/apps/docs/content/docs/self-hosting/deployment/docker.mdx index 7d6d4b9cd..68508e767 100644 --- a/apps/docs/content/docs/self-hosting/deployment/docker.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/docker.mdx @@ -112,6 +112,12 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | | +| `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` | +| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` | +| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` | +| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` | For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment). diff --git a/apps/docs/content/docs/self-hosting/deployment/railway.mdx b/apps/docs/content/docs/self-hosting/deployment/railway.mdx index 81392a37d..501edca1c 100644 --- a/apps/docs/content/docs/self-hosting/deployment/railway.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/railway.mdx @@ -159,6 +159,12 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`| Block new accounts via Microsoft OAuth | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal)| `false` | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | | +| `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` | +| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` | +| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` | +| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN`| Hide the Microsoft signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` | | `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - | | `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` | diff --git a/apps/remix/app/components/forms/signin.tsx b/apps/remix/app/components/forms/signin.tsx index 09e72d7b2..16b9943ca 100644 --- a/apps/remix/app/components/forms/signin.tsx +++ b/apps/remix/app/components/forms/signin.tsx @@ -58,6 +58,7 @@ export type TSignInFormSchema = z.infer; export type SignInFormProps = { className?: string; initialEmail?: string; + isEmailPasswordSigninEnabled?: boolean; isGoogleSSOEnabled?: boolean; isMicrosoftSSOEnabled?: boolean; isOIDCSSOEnabled?: boolean; @@ -68,6 +69,7 @@ export type SignInFormProps = { export const SignInForm = ({ className, initialEmail, + isEmailPasswordSigninEnabled = true, isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, @@ -324,66 +326,78 @@ export const SignInForm = ({
- ( - - - Email - + {isEmailPasswordSigninEnabled && ( + <> + ( + + + Email + - - - + + + - - - )} - /> + + + )} + /> - ( - - - Password - + ( + + + Password + - - - + + + - + -

- - Forgot your password? - -

-
- )} - /> +

+ + Forgot your password? + +

+
+ )} + /> - {turnstileSiteKey && !isTwoFactorAuthenticationDialogOpen && ( - + {turnstileSiteKey && !isTwoFactorAuthenticationDialogOpen && ( + + )} + + + )} - - {!isEmbeddedRedirect && ( <> - {hasSocialAuthEnabled && ( + {isEmailPasswordSigninEnabled && hasSocialAuthEnabled && (
diff --git a/apps/remix/app/routes/_unauthenticated+/forgot-password.tsx b/apps/remix/app/routes/_unauthenticated+/forgot-password.tsx index 68d721c83..c47d0c256 100644 --- a/apps/remix/app/routes/_unauthenticated+/forgot-password.tsx +++ b/apps/remix/app/routes/_unauthenticated+/forgot-password.tsx @@ -1,6 +1,7 @@ +import { isSigninEnabledForProvider } from '@documenso/lib/constants/auth'; import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import { Link } from 'react-router'; +import { Link, redirect } from 'react-router'; import { ForgotPasswordForm } from '~/components/forms/forgot-password'; import { appMetaTags } from '~/utils/meta'; @@ -9,6 +10,14 @@ export function meta() { return appMetaTags(msg`Forgot Password`); } +export async function loader() { + if (!isSigninEnabledForProvider('email')) { + throw redirect('/signin'); + } + + return null; +} + export default function ForgotPasswordPage() { return (
diff --git a/apps/remix/app/routes/_unauthenticated+/reset-password.$token.tsx b/apps/remix/app/routes/_unauthenticated+/reset-password.$token.tsx index 279b68426..0d9ebfe8b 100644 --- a/apps/remix/app/routes/_unauthenticated+/reset-password.$token.tsx +++ b/apps/remix/app/routes/_unauthenticated+/reset-password.$token.tsx @@ -1,3 +1,4 @@ +import { isSigninEnabledForProvider } from '@documenso/lib/constants/auth'; import { getResetTokenValidity } from '@documenso/lib/server-only/user/get-reset-token-validity'; import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; @@ -13,6 +14,10 @@ export function meta() { } export async function loader({ params }: Route.LoaderArgs) { + if (!isSigninEnabledForProvider('email')) { + throw redirect('/signin'); + } + const { token } = params; const isValid = await getResetTokenValidity({ token }); diff --git a/apps/remix/app/routes/_unauthenticated+/reset-password._index.tsx b/apps/remix/app/routes/_unauthenticated+/reset-password._index.tsx index 6e25d11d7..83ed01042 100644 --- a/apps/remix/app/routes/_unauthenticated+/reset-password._index.tsx +++ b/apps/remix/app/routes/_unauthenticated+/reset-password._index.tsx @@ -1,7 +1,8 @@ +import { isSigninEnabledForProvider } from '@documenso/lib/constants/auth'; import { Button } from '@documenso/ui/primitives/button'; import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import { Link } from 'react-router'; +import { Link, redirect } from 'react-router'; import { appMetaTags } from '~/utils/meta'; @@ -9,6 +10,14 @@ export function meta() { return appMetaTags(msg`Reset Password`); } +export async function loader() { + if (!isSigninEnabledForProvider('email')) { + throw redirect('/signin'); + } + + return null; +} + export default function ResetPasswordPage() { return (
diff --git a/apps/remix/app/routes/_unauthenticated+/signin.tsx b/apps/remix/app/routes/_unauthenticated+/signin.tsx index 5ea5a5398..4f9920fc3 100644 --- a/apps/remix/app/routes/_unauthenticated+/signin.tsx +++ b/apps/remix/app/routes/_unauthenticated+/signin.tsx @@ -1,8 +1,11 @@ +import { authClient } from '@documenso/auth/client'; import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session'; import { IS_GOOGLE_SSO_ENABLED, IS_MICROSOFT_SSO_ENABLED, + IS_OIDC_AUTO_REDIRECT_DISABLED, IS_OIDC_SSO_ENABLED, + isSigninEnabledForProvider, isSignupEnabledForProvider, OIDC_PROVIDER_LABEL, } from '@documenso/lib/constants/auth'; @@ -11,6 +14,7 @@ import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; +import { Loader2Icon } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Link, redirect, useSearchParams } from 'react-router'; @@ -28,10 +32,20 @@ export async function loader({ request }: Route.LoaderArgs) { const { isAuthenticated } = await getOptionalSession(request); // SSR env variables. - const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED; - const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED; - const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED; + const isEmailPasswordSigninEnabled = isSigninEnabledForProvider('email'); + const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED && isSigninEnabledForProvider('google'); + const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED && isSigninEnabledForProvider('microsoft'); + const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED && isSigninEnabledForProvider('oidc'); + + // Automatically redirect to OIDC when it is the only enabled signin transport, + // unless the redirect has been explicitly disabled via env. + const isOIDCOnlyTransport = + isOIDCSSOEnabled && !isEmailPasswordSigninEnabled && !isGoogleSSOEnabled && !isMicrosoftSSOEnabled; + + const shouldAutoRedirectToOIDC = isOIDCOnlyTransport && !IS_OIDC_AUTO_REDIRECT_DISABLED; + const oidcProviderLabel = OIDC_PROVIDER_LABEL; + const isSignupEnabled = isSignupEnabledForProvider('email') || (IS_GOOGLE_SSO_ENABLED && isSignupEnabledForProvider('google')) || @@ -47,18 +61,28 @@ export async function loader({ request }: Route.LoaderArgs) { } return { + isEmailPasswordSigninEnabled, isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, isSignupEnabled, oidcProviderLabel, returnTo, + shouldAutoRedirectToOIDC, }; } export default function SignIn({ loaderData }: Route.ComponentProps) { - const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, isSignupEnabled, oidcProviderLabel, returnTo } = - loaderData; + const { + isEmailPasswordSigninEnabled, + isGoogleSSOEnabled, + isMicrosoftSSOEnabled, + isOIDCSSOEnabled, + isSignupEnabled, + oidcProviderLabel, + returnTo, + shouldAutoRedirectToOIDC, + } = loaderData; const { _ } = useLingui(); @@ -76,6 +100,27 @@ export default function SignIn({ loaderData }: Route.ComponentProps) { setIsEmbeddedRedirect(params.get('embedded') === 'true'); }, []); + useEffect(() => { + if (!shouldAutoRedirectToOIDC) { + return; + } + + void authClient.oidc.signIn({ redirectPath: returnTo ?? '/' }); + }, [shouldAutoRedirectToOIDC, returnTo]); + + if (shouldAutoRedirectToOIDC) { + return ( +
+
+ +

+ Redirecting to {oidcProviderLabel || 'OIDC'}... +

+
+
+ ); + } + return (
@@ -95,6 +140,7 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
() .post('/authorize', sValidator('json', ZSignInSchema), async (c) => { const requestMetadata = c.get('requestMetadata'); + if (!isSigninEnabledForProvider('email')) { + throw new AppError(AuthenticationErrorCode.SigninDisabled, { + statusCode: 400, + }); + } + const { email, password, totpCode, backupCode, csrfToken, captchaToken } = c.req.valid('json'); const loginLimitResult = await loginRateLimit.check({ @@ -244,6 +251,12 @@ export const emailPasswordRoute = new Hono() const { password, currentPassword } = c.req.valid('json'); const requestMetadata = c.get('requestMetadata'); + if (!isSigninEnabledForProvider('email')) { + throw new AppError(AuthenticationErrorCode.SigninDisabled, { + statusCode: 400, + }); + } + const { session, user } = await getSession(c); await updatePassword({ @@ -346,6 +359,12 @@ export const emailPasswordRoute = new Hono() .post('/forgot-password', sValidator('json', ZForgotPasswordSchema), async (c) => { const requestMetadata = c.get('requestMetadata'); + if (!isSigninEnabledForProvider('email')) { + throw new AppError(AuthenticationErrorCode.SigninDisabled, { + statusCode: 400, + }); + } + const { email } = c.req.valid('json'); const forgotLimitResult = await forgotPasswordRateLimit.check({ @@ -377,6 +396,12 @@ export const emailPasswordRoute = new Hono() .post('/reset-password', sValidator('json', ZResetPasswordSchema), async (c) => { const requestMetadata = c.get('requestMetadata'); + if (!isSigninEnabledForProvider('email')) { + throw new AppError(AuthenticationErrorCode.SigninDisabled, { + statusCode: 400, + }); + } + const { token, password } = c.req.valid('json'); const resetLimitResult = await resetPasswordRateLimit.check({ diff --git a/packages/lib/constants/auth.ts b/packages/lib/constants/auth.ts index 4768540d4..c4c8727ed 100644 --- a/packages/lib/constants/auth.ts +++ b/packages/lib/constants/auth.ts @@ -41,6 +41,14 @@ export const IS_OIDC_SSO_ENABLED = Boolean( export const OIDC_PROVIDER_LABEL = env('NEXT_PRIVATE_OIDC_PROVIDER_LABEL'); +/** + * Opt-out flag for the automatic OIDC redirect. + * + * When OIDC is the only enabled signin transport we redirect to the provider + * automatically. Set this to "true" to keep rendering the signin page instead. + */ +export const IS_OIDC_AUTO_REDIRECT_DISABLED = env('NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT') === 'true'; + export const USER_SECURITY_AUDIT_LOG_MAP: Record = { ACCOUNT_SSO_LINK: 'Linked account to SSO', ACCOUNT_SSO_UNLINK: 'Unlinked account from SSO', @@ -188,3 +196,22 @@ export const isSignupEnabledForProvider = (provider: 'email' | 'google' | 'micro return env(flagMap[provider]) !== 'true'; }; + +/** + * Check if signin is enabled for the given provider. + * The master switch takes precedence over the per-provider flags. + */ +export const isSigninEnabledForProvider = (provider: 'email' | 'google' | 'microsoft' | 'oidc'): boolean => { + if (env('NEXT_PUBLIC_DISABLE_SIGNIN') === 'true') { + return false; + } + + const flagMap = { + email: 'NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN', + google: 'NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN', + microsoft: 'NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN', + oidc: 'NEXT_PUBLIC_DISABLE_OIDC_SIGNIN', + } as const; + + return env(flagMap[provider]) !== 'true'; +}; diff --git a/packages/lib/translations/de/web.po b/packages/lib/translations/de/web.po index f1f748764..6ec6e94f1 100644 --- a/packages/lib/translations/de/web.po +++ b/packages/lib/translations/de/web.po @@ -8917,6 +8917,11 @@ msgstr "Weiterleitungs-URL" msgid "Redirecting" msgstr "Weiterleitung" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/en/web.po b/packages/lib/translations/en/web.po index 454a46e1e..07cf034bc 100644 --- a/packages/lib/translations/en/web.po +++ b/packages/lib/translations/en/web.po @@ -8908,6 +8908,11 @@ msgstr "Redirect URL" msgid "Redirecting" msgstr "Redirecting" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "Redirecting to {0}..." + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/es/web.po b/packages/lib/translations/es/web.po index 4eef75f10..2d44ea007 100644 --- a/packages/lib/translations/es/web.po +++ b/packages/lib/translations/es/web.po @@ -8917,6 +8917,11 @@ msgstr "URL de redirección" msgid "Redirecting" msgstr "Redireccionando" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/fr/web.po b/packages/lib/translations/fr/web.po index 181b21a64..fb2b175d2 100644 --- a/packages/lib/translations/fr/web.po +++ b/packages/lib/translations/fr/web.po @@ -8917,6 +8917,11 @@ msgstr "URL de redirection" msgid "Redirecting" msgstr "Redirection" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/it/web.po b/packages/lib/translations/it/web.po index d59511b72..1fd209ef4 100644 --- a/packages/lib/translations/it/web.po +++ b/packages/lib/translations/it/web.po @@ -8917,6 +8917,11 @@ msgstr "URL di reindirizzamento" msgid "Redirecting" msgstr "Reindirizzamento" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/ja/web.po b/packages/lib/translations/ja/web.po index 2c710627f..f2fc95818 100644 --- a/packages/lib/translations/ja/web.po +++ b/packages/lib/translations/ja/web.po @@ -8917,6 +8917,11 @@ msgstr "リダイレクト URL" msgid "Redirecting" msgstr "リダイレクト中" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/ko/web.po b/packages/lib/translations/ko/web.po index 802fe31f9..743a6ef9a 100644 --- a/packages/lib/translations/ko/web.po +++ b/packages/lib/translations/ko/web.po @@ -8917,6 +8917,11 @@ msgstr "리디렉션 URL" msgid "Redirecting" msgstr "리디렉션 중" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/nl/web.po b/packages/lib/translations/nl/web.po index 49953db32..4c8da7804 100644 --- a/packages/lib/translations/nl/web.po +++ b/packages/lib/translations/nl/web.po @@ -8917,6 +8917,11 @@ msgstr "Redirect-URL" msgid "Redirecting" msgstr "Doorsturen" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/pl/web.po b/packages/lib/translations/pl/web.po index 010ea2157..de84f4a84 100644 --- a/packages/lib/translations/pl/web.po +++ b/packages/lib/translations/pl/web.po @@ -8918,6 +8918,11 @@ msgstr "Adres URL przekierowania" msgid "Redirecting" msgstr "Przekierowywanie" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/pt-BR/web.po b/packages/lib/translations/pt-BR/web.po index 85490df57..027ae4255 100644 --- a/packages/lib/translations/pt-BR/web.po +++ b/packages/lib/translations/pt-BR/web.po @@ -8908,6 +8908,11 @@ msgstr "URL de Redirecionamento" msgid "Redirecting" msgstr "Redirecionando" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/zh/web.po b/packages/lib/translations/zh/web.po index e107bc848..975a3583c 100644 --- a/packages/lib/translations/zh/web.po +++ b/packages/lib/translations/zh/web.po @@ -8917,6 +8917,11 @@ msgstr "重定向 URL" msgid "Redirecting" msgstr "正在重定向" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index 6757ed47c..d02df2be0 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -93,6 +93,13 @@ declare namespace NodeJS { NEXT_PUBLIC_DISABLE_OIDC_SIGNUP?: string; NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS?: string; + NEXT_PUBLIC_DISABLE_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_OIDC_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT?: string; + NEXT_PRIVATE_BROWSERLESS_URL?: string; NEXT_PRIVATE_JOBS_PROVIDER?: 'inngest' | 'local' | 'bullmq'; diff --git a/render.yaml b/render.yaml index 9a29fae4d..2b82e7f14 100644 --- a/render.yaml +++ b/render.yaml @@ -163,6 +163,18 @@ services: sync: false - key: NEXT_PUBLIC_DISABLE_OIDC_SIGNUP sync: false + - key: NEXT_PUBLIC_DISABLE_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_OIDC_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT + sync: false - key: NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS sync: false diff --git a/turbo.json b/turbo.json index c5c0db0f7..b417941fa 100644 --- a/turbo.json +++ b/turbo.json @@ -53,6 +53,12 @@ "NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP", "NEXT_PUBLIC_DISABLE_OIDC_SIGNUP", "NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS", + "NEXT_PUBLIC_DISABLE_SIGNIN", + "NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN", + "NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN", + "NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN", + "NEXT_PUBLIC_DISABLE_OIDC_SIGNIN", + "NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT", "NEXT_PRIVATE_PLAIN_API_KEY", "NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT", "NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY",