Skip to content
Merged

LDAP #468

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/login/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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",
Expand Down Expand Up @@ -79,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",
Expand Down
8 changes: 8 additions & 0 deletions apps/login/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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",
Expand Down Expand Up @@ -79,6 +80,13 @@
"description": "You need to complete your registration by providing your email address and name."
}
},
"ldap": {
"title": "LDAP Login",
"description": "Enter your LDAP credentials.",
"username": "Username",
"password": "Password",
"submit": "Continue"
},
"mfa": {
"verify": {
"title": "Verify your identity",
Expand Down
8 changes: 8 additions & 0 deletions apps/login/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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",
Expand Down Expand Up @@ -79,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",
Expand Down
8 changes: 8 additions & 0 deletions apps/login/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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",
Expand Down Expand Up @@ -79,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à",
Expand Down
8 changes: 8 additions & 0 deletions apps/login/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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",
Expand Down Expand Up @@ -79,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ść",
Expand Down
8 changes: 8 additions & 0 deletions apps/login/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"idp": {
"title": "Войти через SSO",
"description": "Выберите одного из провайдеров для входа",
"orSignInWith": "или войти через",
"signInWithApple": "Войти через Apple",
"signInWithGoogle": "Войти через Google",
"signInWithAzureAD": "Войти через AzureAD",
Expand Down Expand Up @@ -79,6 +80,13 @@
"description": "Завершите регистрацию вашего аккаунта."
}
},
"ldap": {
"title": "Войти через LDAP",
"description": "Введите ваши учетные данные LDAP.",
"username": "Имя пользователя",
"password": "Пароль",
"submit": "Продолжить"
},
"mfa": {
"verify": {
"title": "Подтвердите вашу личность",
Expand Down
8 changes: 8 additions & 0 deletions apps/login/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"idp": {
"title": "使用 SSO 登录",
"description": "选择以下提供商中的一个进行登录",
"orSignInWith": "或使用以下方式登录",
"signInWithApple": "用 Apple 登录",
"signInWithGoogle": "用 Google 登录",
"signInWithAzureAD": "用 AzureAD 登录",
Expand Down Expand Up @@ -79,6 +80,13 @@
"description": "完成您的账户注册。"
}
},
"ldap": {
"title": "使用 LDAP 登录",
"description": "请输入您的 LDAP 凭据。",
"username": "用户名",
"password": "密码",
"submit": "继续"
},
"mfa": {
"verify": {
"title": "验证您的身份",
Expand Down
56 changes: 56 additions & 0 deletions apps/login/src/app/(login)/idp/ldap/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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 { headers } from "next/headers";

export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
params: Promise<{ provider: string }>;
}) {
const searchParams = await props.searchParams;
const { idpId, 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 (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>
<Translated i18nKey="title" namespace="ldap" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="description" namespace="ldap" />
</p>

<LDAPUsernamePasswordForm
idpId={idpId}
link={link === "true"}
></LDAPUsernamePasswordForm>
</div>
</DynamicTheme>
);
}
10 changes: 6 additions & 4 deletions apps/login/src/app/(login)/loginname/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,17 @@ export default async function Page(props: {
suffix={suffix}
submit={submit}
allowRegister={!!loginSettings?.allowRegister}
>
{identityProviders && loginSettings?.allowExternalIdp && (
></UsernameForm>

{identityProviders && loginSettings?.allowExternalIdp && (
<div className="w-full pt-6 pb-4">
<SignInWithIdp
identityProviders={identityProviders}
requestId={requestId}
organization={organization}
></SignInWithIdp>
)}
</UsernameForm>
</div>
)}
</div>
</DynamicTheme>
);
Expand Down
5 changes: 0 additions & 5 deletions apps/login/src/app/(login)/password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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 { headers } from "next/headers";

export default async function Page(props: {
Expand Down Expand Up @@ -95,10 +94,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"}
/>
)}
</div>
Expand Down
14 changes: 14 additions & 0 deletions apps/login/src/app/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down
106 changes: 106 additions & 0 deletions apps/login/src/components/ldap-username-password-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"use client";

import { createNewSessionForLDAP } from "@/lib/server/idp";
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";
import { Translated } from "./translated";

type Inputs = {
loginName: string;
password: string;
};

type Props = {
idpId: string;
link: boolean;
};

export function LDAPUsernamePasswordForm({ idpId, link }: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
});

const [error, setError] = useState<string>("");

const [loading, setLoading] = useState<boolean>(false);

const router = useRouter();

async function submitUsernamePassword(values: Inputs) {
setError("");
setLoading(true);

const response = await createNewSessionForLDAP({
idpId: idpId,
username: values.loginName,
password: values.password,
link: link,
})
.catch(() => {
setError("Could not start LDAP flow");
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 (
<form className="w-full space-y-4">
<TextInput
type="text"
autoComplete="username"
{...register("loginName", { required: "This field is required" })}
label="Loginname"
data-testid="username-text-input"
/>

<div className={`${error && "transform-gpu animate-shake"}`}>
<TextInput
type="password"
autoComplete="password"
{...register("password", { required: "This field is required" })}
label="Password"
data-testid="password-text-input"
/>
</div>

{error && (
<div className="py-4" data-testid="error">
<Alert>{error}</Alert>
</div>
)}

<div className="mt-8 flex w-full flex-row items-center">
<BackButton data-testid="back-button" />
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit(submitUsernamePassword)}
data-testid="submit-button"
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
<Translated i18nKey="verify.submit" namespace="ldap" />
</Button>
</div>
</form>
);
}
Loading
Loading