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",