From dcbb65d799afa956ac628d7ceb0a287bdd427f8b Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Tue, 2 Sep 2025 04:59:01 +0100 Subject: [PATCH] feat(EE): LDAP integration (#1515) * LDAP - WIP * WIP * add hasGeneratedPassword * fix jotai atom * - don't require password confirmation for MFA is user has auto generated password (LDAP) - cleanups * fix * reorder * update migration * update default * fix type error --- .../src/ee/components/ldap-login-modal.tsx | 124 ++++++++++ apps/client/src/ee/components/sso-login.tsx | 53 +++- .../mfa/components/mfa-backup-code-input.tsx | 1 + .../mfa/components/mfa-backup-codes-modal.tsx | 41 +++- .../src/ee/mfa/components/mfa-challenge.tsx | 1 + .../ee/mfa/components/mfa-disable-modal.tsx | 50 ++-- .../src/ee/mfa/components/mfa-setup-modal.tsx | 1 + .../client/src/ee/mfa/services/mfa-service.ts | 2 +- apps/client/src/ee/mfa/types/mfa.types.ts | 2 +- .../security/components/allowed-domains.tsx | 3 +- .../components/create-sso-provider.tsx | 22 +- .../ee/security/components/sso-ldap-form.tsx | 228 ++++++++++++++++++ .../ee/security/components/sso-oidc-form.tsx | 18 +- .../components/sso-provider-modal.tsx | 5 + .../ee/security/components/sso-saml-form.tsx | 18 +- apps/client/src/ee/security/contants.ts | 1 + .../ee/security/services/ldap-auth-service.ts | 23 ++ .../src/ee/security/types/security.types.ts | 8 + .../features/user/atoms/current-user-atom.ts | 39 ++- .../user/components/account-language.tsx | 2 +- .../src/features/user/types/user.types.ts | 1 + .../components/workspace-name-form.tsx | 3 +- apps/server/package.json | 1 + .../src/core/auth/services/auth.service.ts | 2 + .../migrations/20250831T202306-ldap-auth.ts | 68 ++++++ .../src/database/repos/user/user.repo.ts | 1 + apps/server/src/database/types/db.d.ts | 15 +- apps/server/src/ee | 2 +- pnpm-lock.yaml | 78 +++++- 29 files changed, 723 insertions(+), 90 deletions(-) create mode 100644 apps/client/src/ee/components/ldap-login-modal.tsx create mode 100644 apps/client/src/ee/security/components/sso-ldap-form.tsx create mode 100644 apps/client/src/ee/security/services/ldap-auth-service.ts create mode 100644 apps/server/src/database/migrations/20250831T202306-ldap-auth.ts diff --git a/apps/client/src/ee/components/ldap-login-modal.tsx b/apps/client/src/ee/components/ldap-login-modal.tsx new file mode 100644 index 00000000..9360651d --- /dev/null +++ b/apps/client/src/ee/components/ldap-login-modal.tsx @@ -0,0 +1,124 @@ +import React, { useState } from "react"; +import { Modal, TextInput, PasswordInput, Button, Stack } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { zodResolver } from "mantine-form-zod-resolver"; +import { z } from "zod"; +import { notifications } from "@mantine/notifications"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { IAuthProvider } from "@/ee/security/types/security.types"; +import APP_ROUTE from "@/lib/app-route"; +import { ldapLogin } from "@/ee/security/services/ldap-auth-service"; + +const formSchema = z.object({ + username: z.string().min(1, { message: "Username is required" }), + password: z.string().min(1, { message: "Password is required" }), +}); + +interface LdapLoginModalProps { + opened: boolean; + onClose: () => void; + provider: IAuthProvider; + workspaceId: string; +} + +export function LdapLoginModal({ + opened, + onClose, + provider, + workspaceId, +}: LdapLoginModalProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const form = useForm({ + validate: zodResolver(formSchema), + initialValues: { + username: "", + password: "", + }, + }); + + const handleSubmit = async (values: { + username: string; + password: string; + }) => { + setIsLoading(true); + setError(null); + + try { + const response = await ldapLogin({ + username: values.username, + password: values.password, + providerId: provider.id, + workspaceId, + }); + + // Handle MFA like the regular login + if (response?.userHasMfa) { + onClose(); + navigate(APP_ROUTE.AUTH.MFA_CHALLENGE); + } else if (response?.requiresMfaSetup) { + onClose(); + navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED); + } else { + onClose(); + navigate(APP_ROUTE.HOME); + } + } catch (err: any) { + setIsLoading(false); + const errorMessage = + err.response?.data?.message || "Authentication failed"; + setError(errorMessage); + + notifications.show({ + message: errorMessage, + color: "red", + }); + } + }; + + const handleClose = () => { + form.reset(); + setError(null); + onClose(); + }; + + return ( + +
+ + + + + + + +
+
+ ); +} diff --git a/apps/client/src/ee/components/sso-login.tsx b/apps/client/src/ee/components/sso-login.tsx index 8de93c29..8c96d9c5 100644 --- a/apps/client/src/ee/components/sso-login.tsx +++ b/apps/client/src/ee/components/sso-login.tsx @@ -1,29 +1,62 @@ +import { useState } from "react"; import { useWorkspacePublicDataQuery } from "@/features/workspace/queries/workspace-query.ts"; import { Button, Divider, Stack } from "@mantine/core"; -import { IconLock } from "@tabler/icons-react"; +import { IconLock, IconServer } from "@tabler/icons-react"; import { IAuthProvider } from "@/ee/security/types/security.types.ts"; import { buildSsoLoginUrl } from "@/ee/security/sso.utils.ts"; import { SSO_PROVIDER } from "@/ee/security/contants.ts"; import { GoogleIcon } from "@/components/icons/google-icon.tsx"; import { isCloud } from "@/lib/config.ts"; +import { LdapLoginModal } from "@/ee/components/ldap-login-modal.tsx"; export default function SsoLogin() { const { data, isLoading } = useWorkspacePublicDataQuery(); + const [ldapModalOpened, setLdapModalOpened] = useState(false); + const [selectedLdapProvider, setSelectedLdapProvider] = useState(null); if (!data?.authProviders || data?.authProviders?.length === 0) { return null; } const handleSsoLogin = (provider: IAuthProvider) => { - window.location.href = buildSsoLoginUrl({ - providerId: provider.id, - type: provider.type, - workspaceId: data.id, - }); + if (provider.type === SSO_PROVIDER.LDAP) { + // Open modal for LDAP instead of redirecting + setSelectedLdapProvider(provider); + setLdapModalOpened(true); + } else { + // Redirect for other SSO providers + window.location.href = buildSsoLoginUrl({ + providerId: provider.id, + type: provider.type, + workspaceId: data.id, + }); + } + }; + + const getProviderIcon = (provider: IAuthProvider) => { + if (provider.type === SSO_PROVIDER.GOOGLE) { + return ; + } else if (provider.type === SSO_PROVIDER.LDAP) { + return ; + } else { + return ; + } }; return ( <> + {selectedLdapProvider && ( + { + setLdapModalOpened(false); + setSelectedLdapProvider(null); + }} + provider={selectedLdapProvider} + workspaceId={data.id} + /> + )} + {(isCloud() || data.hasLicenseKey) && ( <> @@ -31,13 +64,7 @@ export default function SsoLogin() {