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 (
+
+ );
+}
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 (
-
);
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 && (