From c00bab46e0842489cc42d0ad838c76d9f4afcf6d Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 13:18:37 +0200 Subject: [PATCH 01/10] fix hydration issue for IDPs, i18n --- apps/login/locales/de.json | 1 + apps/login/locales/en.json | 1 + apps/login/locales/es.json | 1 + apps/login/locales/it.json | 1 + apps/login/locales/pl.json | 1 + apps/login/locales/ru.json | 1 + apps/login/locales/zh.json | 1 + apps/login/src/app/(login)/loginname/page.tsx | 10 ++++++---- apps/login/src/components/sign-in-with-idp.tsx | 4 ++++ apps/login/src/components/username-form.tsx | 3 --- 10 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index 8b3d4b311e..7606ef1a0a 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -37,6 +37,7 @@ "idp": { "title": "Mit SSO anmelden", "description": "Wählen Sie einen der folgenden Anbieter, um sich anzumelden", + "orSignInWith": "oder melden Sie sich an mit", "signInWithApple": "Mit Apple anmelden", "signInWithGoogle": "Mit Google anmelden", "signInWithAzureAD": "Mit AzureAD anmelden", diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index daaaeba108..e2032ee21a 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -37,6 +37,7 @@ "idp": { "title": "Sign in with SSO", "description": "Select one of the following providers to sign in", + "orSignInWith": "or sign in with", "signInWithApple": "Sign in with Apple", "signInWithGoogle": "Sign in with Google", "signInWithAzureAD": "Sign in with AzureAD", diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index b7dd57b4c0..f5a0b05801 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -37,6 +37,7 @@ "idp": { "title": "Iniciar sesión con SSO", "description": "Selecciona uno de los siguientes proveedores para iniciar sesión", + "orSignInWith": "o iniciar sesión con", "signInWithApple": "Iniciar sesión con Apple", "signInWithGoogle": "Iniciar sesión con Google", "signInWithAzureAD": "Iniciar sesión con AzureAD", diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index f476da3402..b12d0e978d 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -37,6 +37,7 @@ "idp": { "title": "Accedi con SSO", "description": "Seleziona uno dei seguenti provider per accedere", + "orSignInWith": "o accedi con", "signInWithApple": "Accedi con Apple", "signInWithGoogle": "Accedi con Google", "signInWithAzureAD": "Accedi con AzureAD", diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index 4dd607f3cb..6ce67bada9 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -37,6 +37,7 @@ "idp": { "title": "Zaloguj się za pomocą SSO", "description": "Wybierz jednego z poniższych dostawców, aby się zalogować", + "orSignInWith": "lub zaloguj się przez", "signInWithApple": "Zaloguj się przez Apple", "signInWithGoogle": "Zaloguj się przez Google", "signInWithAzureAD": "Zaloguj się przez AzureAD", diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index e8bbac212b..77d0314199 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -37,6 +37,7 @@ "idp": { "title": "Войти через SSO", "description": "Выберите одного из провайдеров для входа", + "orSignInWith": "или войти через", "signInWithApple": "Войти через Apple", "signInWithGoogle": "Войти через Google", "signInWithAzureAD": "Войти через AzureAD", diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index 7bc4ecf68a..e03a9fd62d 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -37,6 +37,7 @@ "idp": { "title": "使用 SSO 登录", "description": "选择以下提供商中的一个进行登录", + "orSignInWith": "或使用以下方式登录", "signInWithApple": "用 Apple 登录", "signInWithGoogle": "用 Google 登录", "signInWithAzureAD": "用 AzureAD 登录", diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index 79372729c4..9a404d5068 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -74,15 +74,17 @@ export default async function Page(props: { suffix={suffix} submit={submit} allowRegister={!!loginSettings?.allowRegister} - > - {identityProviders && ( + > + + {identityProviders && ( +
- )} - +
+ )} ); diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index 7632a29cc1..7811c0bff3 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -6,6 +6,7 @@ import { IdentityProvider, IdentityProviderType, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useTranslations } from "next-intl"; import { ReactNode, useActionState } from "react"; import { Alert } from "./alert"; import { SignInWithIdentityProviderProps } from "./idps/base-button"; @@ -31,6 +32,7 @@ export function SignInWithIdp({ linkOnly, }: Readonly) { const [state, action, _isPending] = useActionState(redirectToIdp, {}); + const t = useTranslations("idp"); const renderIDPButton = (idp: IdentityProvider, index: number) => { const { id, name, type } = idp; @@ -53,6 +55,7 @@ export function SignInWithIdp({ [IdentityProviderType.GITLAB]: SignInWithGitlab, [IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab, [IdentityProviderType.SAML]: SignInWithGeneric, + [IdentityProviderType.LDAP]: SignInWithGeneric, }; const Component = components[type]; @@ -74,6 +77,7 @@ export function SignInWithIdp({ return (
+

{t("orSignInWith")}

{!!identityProviders.length && identityProviders?.map(renderIDPButton)} {state?.error && (
diff --git a/apps/login/src/components/username-form.tsx b/apps/login/src/components/username-form.tsx index 6801f6b274..a239da3528 100644 --- a/apps/login/src/components/username-form.tsx +++ b/apps/login/src/components/username-form.tsx @@ -137,9 +137,6 @@ export function UsernameForm({ {error}
)} - -
{children}
-
From 2288e6ca92f622da833ebbe7346ae6600a32ca79 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 13:41:18 +0200 Subject: [PATCH 02/10] cleanup routes --- apps/login/src/app/login/route.ts | 14 ++++++++++++++ apps/login/src/lib/idp.ts | 2 ++ apps/login/src/lib/server/idp.ts | 4 ++++ apps/login/src/lib/zitadel.ts | 1 - 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 0e181b76f1..cdb25bae65 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -24,6 +24,7 @@ import { } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; import { DEFAULT_CSP } from "../../../constants/csp"; @@ -191,6 +192,19 @@ export async function GET(request: NextRequest) { const origin = request.nextUrl.origin; const identityProviderType = identityProviders[0].type; + + if (identityProviderType === IdentityProviderType.LDAP) { + const ldapUrl = constructUrl(request, "/ldap"); + if (authRequest.id) { + ldapUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); + } + if (organization) { + ldapUrl.searchParams.set("organization", organization); + } + + return NextResponse.redirect(ldapUrl); + } + let provider = idpTypeToSlug(identityProviderType); const params = new URLSearchParams(); diff --git a/apps/login/src/lib/idp.ts b/apps/login/src/lib/idp.ts index 1d4b82951a..66b3dfa594 100644 --- a/apps/login/src/lib/idp.ts +++ b/apps/login/src/lib/idp.ts @@ -24,6 +24,8 @@ export function idpTypeToSlug(idpType: IdentityProviderType) { return "oauth"; case IdentityProviderType.OIDC: return "oidc"; + case IdentityProviderType.LDAP: + return "ldap"; default: throw new Error("Unknown identity provider type"); } diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index 5cac537690..33e2990bdc 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -30,6 +30,10 @@ export async function redirectToIdp( if (requestId) params.set("requestId", requestId); if (organization) params.set("organization", organization); + if (provider === "ldap") { + redirect("/idp/ldap?linkOnly=" + linkOnly + "&" + params.toString()); + } + const response = await startIDPFlow({ idpId, successUrl: `/idp/${provider}/success?` + params.toString(), diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index d1fe83434d..b7f0f9a059 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -908,7 +908,6 @@ export async function startIdentityProviderFlow({ urls, }: { serviceUrl: string; - idpId: string; urls: RedirectURLsJson; }) { From 9d975bb39a9889eef504ae33d124d53dcc635986 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 15:48:54 +0200 Subject: [PATCH 03/10] ldap components --- apps/login/src/app/(login)/password/page.tsx | 7 +- apps/login/src/components/login-passkey.tsx | 12 +- apps/login/src/components/password-form.tsx | 4 - .../src/components/username-password-form.tsx | 120 ++++++++++++++++++ 4 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 apps/login/src/components/username-password-form.tsx diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx index 506454a275..29a49c61f2 100644 --- a/apps/login/src/app/(login)/password/page.tsx +++ b/apps/login/src/app/(login)/password/page.tsx @@ -10,7 +10,6 @@ import { getLoginSettings, } from "@/lib/zitadel"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; -import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; @@ -22,7 +21,7 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "password" }); const tError = await getTranslations({ locale, namespace: "error" }); - let { loginName, organization, requestId, alt } = searchParams; + let { loginName, organization, requestId } = searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -93,10 +92,6 @@ export default async function Page(props: { requestId={requestId} organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user loginSettings={loginSettings} - promptPasswordless={ - loginSettings?.passkeysType == PasskeysType.ALLOWED - } - isAlternative={alt === "true"} /> )}
diff --git a/apps/login/src/components/login-passkey.tsx b/apps/login/src/components/login-passkey.tsx index b3f0b1212f..b364a9de45 100644 --- a/apps/login/src/components/login-passkey.tsx +++ b/apps/login/src/components/login-passkey.tsx @@ -210,26 +210,26 @@ export function LoginPasskey({ type="button" variant={ButtonVariants.Secondary} onClick={() => { - const params: any = { alt: "true" }; + const params = new URLSearchParams(); if (loginName) { - params.loginName = loginName; + params.append("loginName", loginName); } if (sessionId) { - params.sessionId = sessionId; + params.append("sessionId", sessionId); } if (requestId) { - params.requestId = requestId; + params.append("requestId", requestId); } if (organization) { - params.organization = organization; + params.append("organization", organization); } return router.push( - "/password?" + new URLSearchParams(params), // alt is set because password is requested as alternative auth method, so passwordless prompt can be escaped + "/password?" + params, // alt is set because password is requested as alternative auth method, so passwordless prompt can be escaped ); }} data-testid="password-button" diff --git a/apps/login/src/components/password-form.tsx b/apps/login/src/components/password-form.tsx index 17461644d8..a9b0a01316 100644 --- a/apps/login/src/components/password-form.tsx +++ b/apps/login/src/components/password-form.tsx @@ -23,8 +23,6 @@ type Props = { loginName: string; organization?: string; requestId?: string; - isAlternative?: boolean; // whether password was requested as alternative auth method - promptPasswordless?: boolean; }; export function PasswordForm({ @@ -32,8 +30,6 @@ export function PasswordForm({ loginName, organization, requestId, - promptPasswordless, - isAlternative, }: Props) { const t = useTranslations("password"); diff --git a/apps/login/src/components/username-password-form.tsx b/apps/login/src/components/username-password-form.tsx new file mode 100644 index 0000000000..fb6a98d8c3 --- /dev/null +++ b/apps/login/src/components/username-password-form.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { sendPassword } from "@/lib/server/password"; +import { create } from "@zitadel/client"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; + +type Inputs = { + loginName: string; + password: string; +}; + +type Props = { + loginSettings: LoginSettings | undefined; + loginName: string; + organization?: string; + requestId?: string; +}; + +export function UsernamePasswordForm({ + loginSettings, + loginName, + organization, + requestId, +}: Props) { + const t = useTranslations("password"); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + }); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitUsernamePassword(values: Inputs) { + setError(""); + setLoading(true); + + const response = await sendPassword({ + loginName, + organization, + checks: create(ChecksSchema, { + password: { password: values.password }, + }), + requestId, + }) + .catch(() => { + setError("Could not verify password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + } + + return ( +
+ + +
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+ + ); +} From 7ea2103b57d6e31c6737d0afd592cff89f9af25c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 28 May 2025 10:56:46 +0200 Subject: [PATCH 04/10] ldap page, start idp flow --- .../app/(login)/authenticator/set/page.tsx | 2 +- apps/login/src/app/(login)/idp/ldap/page.tsx | 55 ++++++++++++ .../src/components/username-password-form.tsx | 24 ++--- apps/login/src/lib/server/idp.ts | 90 +++++++++++++++---- apps/login/src/lib/zitadel.ts | 28 ++++++ 5 files changed, 167 insertions(+), 32 deletions(-) create mode 100644 apps/login/src/app/(login)/idp/ldap/page.tsx diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 5a8dfe810d..82b494f200 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -184,7 +184,7 @@ export default async function Page(props: { > )} - {loginSettings?.allowExternalIdp && identityProviders && ( + {loginSettings?.allowExternalIdp && !!identityProviders.length && ( <> {identityProviders.length && (
diff --git a/apps/login/src/app/(login)/idp/ldap/page.tsx b/apps/login/src/app/(login)/idp/ldap/page.tsx new file mode 100644 index 0000000000..6a54c2d648 --- /dev/null +++ b/apps/login/src/app/(login)/idp/ldap/page.tsx @@ -0,0 +1,55 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { UsernamePasswordForm } from "@/components/username-password-form"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; + params: Promise<{ provider: string }>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "ldap" }); + const { idpId, requestId, organization, link } = searchParams; + + if (!idpId) { + throw new Error("No idpId provided in searchParams"); + } + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + defaultOrganization = org.id; + } + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + // return login failed if no linking or creation is allowed and no user was found + return ( + +
+

{t("title")}

+

{t("description")}

+ + +
+
+ ); +} diff --git a/apps/login/src/components/username-password-form.tsx b/apps/login/src/components/username-password-form.tsx index fb6a98d8c3..4c5543866d 100644 --- a/apps/login/src/components/username-password-form.tsx +++ b/apps/login/src/components/username-password-form.tsx @@ -1,9 +1,6 @@ "use client"; -import { sendPassword } from "@/lib/server/password"; -import { create } from "@zitadel/client"; -import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { createNewSessionForLDAP } from "@/lib/server/idp"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -20,17 +17,15 @@ type Inputs = { }; type Props = { - loginSettings: LoginSettings | undefined; - loginName: string; organization?: string; requestId?: string; + idpId: string; }; export function UsernamePasswordForm({ - loginSettings, - loginName, organization, requestId, + idpId, }: Props) { const t = useTranslations("password"); @@ -48,13 +43,10 @@ export function UsernamePasswordForm({ setError(""); setLoading(true); - const response = await sendPassword({ - loginName, - organization, - checks: create(ChecksSchema, { - password: { password: values.password }, - }), - requestId, + const response = await createNewSessionForLDAP({ + idpId: idpId, + username: values.loginName, + password: values.password, }) .catch(() => { setError("Could not verify password"); @@ -75,7 +67,7 @@ export function UsernamePasswordForm({ } return ( -
+ { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + if (!host) { + return { error: "Could not get host" }; + } + const params = new URLSearchParams(); const linkOnly = formData.get("linkOnly") === "true"; @@ -26,52 +34,54 @@ export async function redirectToIdp( const idpId = formData.get("id") as string; const provider = formData.get("provider") as string; + // const username = formData.get("username") as string; + // const password = formData.get("password") as string; + if (linkOnly) params.set("link", "true"); if (requestId) params.set("requestId", requestId); if (organization) params.set("organization", organization); + // redirect to LDAP page where username and password is requested if (provider === "ldap") { - redirect("/idp/ldap?linkOnly=" + linkOnly + "&" + params.toString()); + redirect(`/idp/ldap?` + params.toString()); } const response = await startIDPFlow({ + serviceUrl, + host, idpId, successUrl: `/idp/${provider}/success?` + params.toString(), failureUrl: `/idp/${provider}/failure?` + params.toString(), }); - if (response && "error" in response && response?.error) { - return { error: response.error }; + if (!response) { + return { error: "Could not start IDP flow" }; } if (response && "redirect" in response && response?.redirect) { redirect(response.redirect); } + + return { error: "Unexpected response from IDP flow" }; } export type StartIDPFlowCommand = { + serviceUrl: string; + host: string; idpId: string; successUrl: string; failureUrl: string; }; -export async function startIDPFlow(command: StartIDPFlowCommand) { - const _headers = await headers(); - const { serviceUrl } = getServiceUrlFromHeaders(_headers); - const host = _headers.get("host"); - - if (!host) { - return { error: "Could not get host" }; - } - +async function startIDPFlow(command: StartIDPFlowCommand) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; return startIdentityProviderFlow({ - serviceUrl, + serviceUrl: command.serviceUrl, idpId: command.idpId, urls: { - successUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}${command.successUrl}`, - failureUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}${command.failureUrl}`, + successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`, + failureUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.failureUrl}`, }, }).then((response) => { if ( @@ -178,3 +188,53 @@ export async function createNewSessionFromIdpIntent( return { redirect: url }; } } + +type createNewSessionForLDAPCommand = { + username: string; + password: string; + idpId: string; +}; + +export async function createNewSessionForLDAP( + command: createNewSessionForLDAPCommand, +) { + const _headers = await headers(); + + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "Could not get domain" }; + } + + if (!command.username || !command.password) { + return { error: "No username or password provided" }; + } + + const response = await startLDAPIdentityProviderFlow({ + serviceUrl, + idpId: command.idpId, + username: command.username, + password: command.password, + }); + + if ( + !response || + response.nextStep.case !== "idpIntent" || + !response.nextStep.value + ) { + return { error: "Could not start LDAP identity provider flow" }; + } + + const { userId, idpIntentId, idpIntentToken } = response.nextStep.value; + + return { + redirect: + `/idp/ldap/success?` + + new URLSearchParams({ + userId, + id: idpIntentId, + token: idpIntentToken, + }).toString(), + }; +} diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index bdf46a5905..5cc4181e1f 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -910,6 +910,34 @@ export async function startIdentityProviderFlow({ }); } +export async function startLDAPIdentityProviderFlow({ + serviceUrl, + idpId, + username, + password, +}: { + serviceUrl: string; + idpId: string; + username: string; + password: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.startIdentityProviderIntent({ + idpId, + content: { + case: "ldap", + value: { + username, + password, + }, + }, + }); +} + export async function getAuthRequest({ serviceUrl, authRequestId, From 0818661496630e3355a4e5ccd4ecc2d64879a746 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 28 May 2025 11:47:42 +0200 Subject: [PATCH 05/10] ldap --- apps/login/locales/en.json | 7 +++++++ apps/login/src/app/(login)/idp/ldap/page.tsx | 6 +++--- ...e-password-form.tsx => ldap-username-password-form.tsx} | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) rename apps/login/src/components/{username-password-form.tsx => ldap-username-password-form.tsx} (98%) diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index e2032ee21a..81c93bb549 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -64,6 +64,13 @@ "description": "An error occurred while trying to link your account." } }, + "ldap": { + "title": "LDAP Login", + "description": "Enter your LDAP credentials.", + "username": "Username", + "password": "Password", + "submit": "Continue" + }, "mfa": { "verify": { "title": "Verify your identity", diff --git a/apps/login/src/app/(login)/idp/ldap/page.tsx b/apps/login/src/app/(login)/idp/ldap/page.tsx index 6a54c2d648..67e6bbe4a7 100644 --- a/apps/login/src/app/(login)/idp/ldap/page.tsx +++ b/apps/login/src/app/(login)/idp/ldap/page.tsx @@ -1,5 +1,5 @@ import { DynamicTheme } from "@/components/dynamic-theme"; -import { UsernamePasswordForm } from "@/components/username-password-form"; +import { LDAPUsernamePasswordForm } from "@/components/ldap-username-password-form"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; @@ -44,11 +44,11 @@ export default async function Page(props: {

{t("title")}

{t("description")}

- + >
); diff --git a/apps/login/src/components/username-password-form.tsx b/apps/login/src/components/ldap-username-password-form.tsx similarity index 98% rename from apps/login/src/components/username-password-form.tsx rename to apps/login/src/components/ldap-username-password-form.tsx index 4c5543866d..5350281053 100644 --- a/apps/login/src/components/username-password-form.tsx +++ b/apps/login/src/components/ldap-username-password-form.tsx @@ -22,7 +22,7 @@ type Props = { idpId: string; }; -export function UsernamePasswordForm({ +export function LDAPUsernamePasswordForm({ organization, requestId, idpId, From 2ba8311cd6f9603c1a796f4aece9ebd4ef4b800d Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 2 Jun 2025 11:00:05 +0200 Subject: [PATCH 06/10] idpId param, error msg --- apps/login/src/components/ldap-username-password-form.tsx | 2 +- apps/login/src/lib/server/idp.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/login/src/components/ldap-username-password-form.tsx b/apps/login/src/components/ldap-username-password-form.tsx index 5350281053..b3dcaabd58 100644 --- a/apps/login/src/components/ldap-username-password-form.tsx +++ b/apps/login/src/components/ldap-username-password-form.tsx @@ -49,7 +49,7 @@ export function LDAPUsernamePasswordForm({ password: values.password, }) .catch(() => { - setError("Could not verify password"); + setError("Could not start LDAP flow"); return; }) .finally(() => { diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index 1fb5173fe2..367824331f 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -43,6 +43,7 @@ export async function redirectToIdp( // redirect to LDAP page where username and password is requested if (provider === "ldap") { + params.set("idpId", idpId); redirect(`/idp/ldap?` + params.toString()); } From bfb7928b6b24350a904fb6283f2810c1541b758e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 25 Jun 2025 08:14:04 +0200 Subject: [PATCH 07/10] missing i18n, cleanup --- apps/login/locales/de.json | 7 +++++++ apps/login/locales/es.json | 7 +++++++ apps/login/locales/it.json | 7 +++++++ apps/login/locales/pl.json | 7 +++++++ apps/login/locales/ru.json | 7 +++++++ apps/login/locales/zh.json | 7 +++++++ apps/login/src/app/(login)/idp/ldap/page.tsx | 20 +++++++++---------- .../ldap-username-password-form.tsx | 16 ++++----------- 8 files changed, 55 insertions(+), 23 deletions(-) diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index ecb4740682..75897a628e 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -80,6 +80,13 @@ "description": "Bitte vervollständige die Registrierung, um dein Konto zu erstellen." } }, + "ldap": { + "title": "LDAP Login", + "description": "Geben Sie Ihre LDAP-Anmeldedaten ein.", + "username": "Benutzername", + "password": "Passwort", + "submit": "Weiter" + }, "mfa": { "verify": { "title": "Bestätigen Sie Ihre Identität", diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 40e3392e4a..fe88bb94c6 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -80,6 +80,13 @@ "description": "Para completar el registro, debes establecer una contraseña." } }, + "ldap": { + "title": "Iniciar sesión con LDAP", + "description": "Introduce tus credenciales LDAP.", + "username": "Nombre de usuario", + "password": "Contraseña", + "submit": "Continuar" + }, "mfa": { "verify": { "title": "Verifica tu identidad", diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 6f562f0974..1229a1a4c0 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -80,6 +80,13 @@ "description": "Completa la registrazione del tuo account." } }, + "ldap": { + "title": "Accedi con LDAP", + "description": "Inserisci le tue credenziali LDAP.", + "username": "Nome utente", + "password": "Password", + "submit": "Continua" + }, "mfa": { "verify": { "title": "Verifica la tua identità", diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index 58c77b1d09..9fea6a19fa 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -80,6 +80,13 @@ "description": "Ukończ rejestrację swojego konta." } }, + "ldap": { + "title": "Zaloguj się przez LDAP", + "description": "Wprowadź swoje dane logowania LDAP.", + "username": "Nazwa użytkownika", + "password": "Hasło", + "submit": "Kontynuuj" + }, "mfa": { "verify": { "title": "Zweryfikuj swoją tożsamość", diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index b73f9b99c6..e745f1ae59 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -80,6 +80,13 @@ "description": "Завершите регистрацию вашего аккаунта." } }, + "ldap": { + "title": "Войти через LDAP", + "description": "Введите ваши учетные данные LDAP.", + "username": "Имя пользователя", + "password": "Пароль", + "submit": "Продолжить" + }, "mfa": { "verify": { "title": "Подтвердите вашу личность", diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index 1023660882..5a9cb3a4eb 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -80,6 +80,13 @@ "description": "完成您的账户注册。" } }, + "ldap": { + "title": "使用 LDAP 登录", + "description": "请输入您的 LDAP 凭据。", + "username": "用户名", + "password": "密码", + "submit": "继续" + }, "mfa": { "verify": { "title": "验证您的身份", diff --git a/apps/login/src/app/(login)/idp/ldap/page.tsx b/apps/login/src/app/(login)/idp/ldap/page.tsx index 67e6bbe4a7..105c6832e6 100644 --- a/apps/login/src/app/(login)/idp/ldap/page.tsx +++ b/apps/login/src/app/(login)/idp/ldap/page.tsx @@ -1,9 +1,9 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { LDAPUsernamePasswordForm } from "@/components/ldap-username-password-form"; +import { Translated } from "@/components/translated"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { @@ -11,9 +11,7 @@ export default async function Page(props: { params: Promise<{ provider: string }>; }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "ldap" }); - const { idpId, requestId, organization, link } = searchParams; + const { idpId, organization, link } = searchParams; if (!idpId) { throw new Error("No idpId provided in searchParams"); @@ -41,14 +39,14 @@ export default async function Page(props: { return (
-

{t("title")}

-

{t("description")}

+

+ +

+

+ +

- +
); diff --git a/apps/login/src/components/ldap-username-password-form.tsx b/apps/login/src/components/ldap-username-password-form.tsx index b3dcaabd58..b7653c5782 100644 --- a/apps/login/src/components/ldap-username-password-form.tsx +++ b/apps/login/src/components/ldap-username-password-form.tsx @@ -1,7 +1,6 @@ "use client"; import { createNewSessionForLDAP } from "@/lib/server/idp"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -10,6 +9,7 @@ import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = { loginName: string; @@ -17,18 +17,10 @@ type Inputs = { }; type Props = { - organization?: string; - requestId?: string; idpId: string; }; -export function LDAPUsernamePasswordForm({ - organization, - requestId, - idpId, -}: Props) { - const t = useTranslations("password"); - +export function LDAPUsernamePasswordForm({ idpId }: Props) { const { register, handleSubmit, formState } = useForm({ mode: "onBlur", }); @@ -72,7 +64,7 @@ export function LDAPUsernamePasswordForm({ type="text" autoComplete="username" {...register("loginName", { required: "This field is required" })} - label={"Loginname"} + label="Loginname" data-testid="username-text-input" /> @@ -104,7 +96,7 @@ export function LDAPUsernamePasswordForm({ data-testid="submit-button" > {loading && } - {t("verify.submit")} +
From 94665022e5a3c0704679537fef9b8b496f1f0014 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 25 Jun 2025 08:19:16 +0200 Subject: [PATCH 08/10] link property for ldap --- apps/login/src/app/(login)/idp/ldap/page.tsx | 5 ++++- .../ldap-username-password-form.tsx | 4 +++- apps/login/src/lib/server/idp.ts | 22 ++++++++++--------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/login/src/app/(login)/idp/ldap/page.tsx b/apps/login/src/app/(login)/idp/ldap/page.tsx index 105c6832e6..372c814525 100644 --- a/apps/login/src/app/(login)/idp/ldap/page.tsx +++ b/apps/login/src/app/(login)/idp/ldap/page.tsx @@ -46,7 +46,10 @@ export default async function Page(props: {

- + ); diff --git a/apps/login/src/components/ldap-username-password-form.tsx b/apps/login/src/components/ldap-username-password-form.tsx index b7653c5782..fe653ba077 100644 --- a/apps/login/src/components/ldap-username-password-form.tsx +++ b/apps/login/src/components/ldap-username-password-form.tsx @@ -18,9 +18,10 @@ type Inputs = { type Props = { idpId: string; + link?: boolean; }; -export function LDAPUsernamePasswordForm({ idpId }: Props) { +export function LDAPUsernamePasswordForm({ idpId, link }: Props) { const { register, handleSubmit, formState } = useForm({ mode: "onBlur", }); @@ -39,6 +40,7 @@ export function LDAPUsernamePasswordForm({ idpId }: Props) { idpId: idpId, username: values.loginName, password: values.password, + link: link, }) .catch(() => { setError("Could not start LDAP flow"); diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index 30ee0aa6fb..1925fe43d6 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -34,9 +34,6 @@ export async function redirectToIdp( const idpId = formData.get("id") as string; const provider = formData.get("provider") as string; - // const username = formData.get("username") as string; - // const password = formData.get("password") as string; - if (linkOnly) params.set("link", "true"); if (requestId) params.set("requestId", requestId); if (organization) params.set("organization", organization); @@ -194,6 +191,7 @@ type createNewSessionForLDAPCommand = { username: string; password: string; idpId: string; + link: boolean; }; export async function createNewSessionForLDAP( @@ -229,13 +227,17 @@ export async function createNewSessionForLDAP( const { userId, idpIntentId, idpIntentToken } = response.nextStep.value; + const params = new URLSearchParams({ + userId, + id: idpIntentId, + token: idpIntentToken, + }); + + if (command.link) { + params.set("link", "true"); + } + return { - redirect: - `/idp/ldap/success?` + - new URLSearchParams({ - userId, - id: idpIntentId, - token: idpIntentToken, - }).toString(), + redirect: `/idp/ldap/success?` + params.toString(), }; } From f33add736310eb8594662caca1bc555be089c6ce Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 25 Jun 2025 08:21:08 +0200 Subject: [PATCH 09/10] prop --- apps/login/src/components/ldap-username-password-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/components/ldap-username-password-form.tsx b/apps/login/src/components/ldap-username-password-form.tsx index fe653ba077..f7ea9aea0e 100644 --- a/apps/login/src/components/ldap-username-password-form.tsx +++ b/apps/login/src/components/ldap-username-password-form.tsx @@ -18,7 +18,7 @@ type Inputs = { type Props = { idpId: string; - link?: boolean; + link: boolean; }; export function LDAPUsernamePasswordForm({ idpId, link }: Props) { From 82cf493fe17f8070f46fd9a0401c50eae6543e77 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 25 Jun 2025 08:34:41 +0200 Subject: [PATCH 10/10] i18n --- apps/login/src/components/sign-in-with-idp.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index 13b179071c..ec9cfb36f8 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -6,7 +6,6 @@ import { IdentityProvider, IdentityProviderType, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; -import { useTranslations } from "next-intl"; import { ReactNode, useActionState } from "react"; import { Alert } from "./alert"; import { SignInWithIdentityProviderProps } from "./idps/base-button"; @@ -16,6 +15,7 @@ import { SignInWithGeneric } from "./idps/sign-in-with-generic"; import { SignInWithGithub } from "./idps/sign-in-with-github"; import { SignInWithGitlab } from "./idps/sign-in-with-gitlab"; import { SignInWithGoogle } from "./idps/sign-in-with-google"; +import { Translated } from "./translated"; export interface SignInWithIDPProps { children?: ReactNode; @@ -32,7 +32,6 @@ export function SignInWithIdp({ linkOnly, }: Readonly) { const [state, action, _isPending] = useActionState(redirectToIdp, {}); - const t = useTranslations("idp"); const renderIDPButton = (idp: IdentityProvider, index: number) => { const { id, name, type } = idp; @@ -78,7 +77,9 @@ export function SignInWithIdp({ return (
-

{t("orSignInWith")}

+

+ +

{!!identityProviders.length && identityProviders?.map(renderIDPButton)} {state?.error && (