mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 17:57:33 +00:00
Merge branch 'main' into saml-idp
This commit is contained in:
BIN
.github/custom-i18n.png
vendored
Normal file
BIN
.github/custom-i18n.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
11
README.md
11
README.md
@@ -66,8 +66,8 @@ You can already use the current state, and extend it with your needs.
|
|||||||
- [x] Generic OIDC
|
- [x] Generic OIDC
|
||||||
- [x] Generic OAuth
|
- [x] Generic OAuth
|
||||||
- [x] Generic JWT
|
- [x] Generic JWT
|
||||||
- [ ] LDAP
|
- [x] LDAP
|
||||||
- [ ] SAML SP
|
- [x] SAML SP
|
||||||
- Multifactor Registration an Login
|
- Multifactor Registration an Login
|
||||||
- [x] Passkeys
|
- [x] Passkeys
|
||||||
- [x] TOTP
|
- [x] TOTP
|
||||||
@@ -143,6 +143,13 @@ You can already use the current state, and extend it with your needs.
|
|||||||
|
|
||||||
You can find a more detailed documentation of the different pages [here](./apps/login/readme.md).
|
You can find a more detailed documentation of the different pages [here](./apps/login/readme.md).
|
||||||
|
|
||||||
|
#### Custom translations
|
||||||
|
|
||||||
|
The new login uses the [SettingsApi](https://zitadel.com/docs/apis/resources/settings_service_v2/settings-service-get-hosted-login-translation) to load custom translations.
|
||||||
|
Translations can be overriden at both the instance and organization levels.
|
||||||
|
To find the keys more easily, you can inspect the HTML and search for a `data-i18n-key` attribute, or look at the defaults in `/apps/login/locales/[locale].ts`.
|
||||||
|

|
||||||
|
|
||||||
## Tooling
|
## Tooling
|
||||||
|
|
||||||
- [TypeScript](https://www.typescriptlang.org/) for static type checking
|
- [TypeScript](https://www.typescriptlang.org/) for static type checking
|
||||||
|
@@ -49,6 +49,7 @@
|
|||||||
"idp": {
|
"idp": {
|
||||||
"title": "Mit SSO anmelden",
|
"title": "Mit SSO anmelden",
|
||||||
"description": "Wählen Sie einen der folgenden Anbieter, um sich anzumelden",
|
"description": "Wählen Sie einen der folgenden Anbieter, um sich anzumelden",
|
||||||
|
"orSignInWith": "oder melden Sie sich an mit",
|
||||||
"signInWithApple": "Mit Apple anmelden",
|
"signInWithApple": "Mit Apple anmelden",
|
||||||
"signInWithGoogle": "Mit Google anmelden",
|
"signInWithGoogle": "Mit Google anmelden",
|
||||||
"signInWithAzureAD": "Mit AzureAD anmelden",
|
"signInWithAzureAD": "Mit AzureAD anmelden",
|
||||||
@@ -79,6 +80,13 @@
|
|||||||
"description": "Bitte vervollständige die Registrierung, um dein Konto zu erstellen."
|
"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": {
|
"mfa": {
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Bestätigen Sie Ihre Identität",
|
"title": "Bestätigen Sie Ihre Identität",
|
||||||
|
@@ -49,6 +49,7 @@
|
|||||||
"idp": {
|
"idp": {
|
||||||
"title": "Sign in with SSO",
|
"title": "Sign in with SSO",
|
||||||
"description": "Select one of the following providers to sign in",
|
"description": "Select one of the following providers to sign in",
|
||||||
|
"orSignInWith": "or sign in with",
|
||||||
"signInWithApple": "Sign in with Apple",
|
"signInWithApple": "Sign in with Apple",
|
||||||
"signInWithGoogle": "Sign in with Google",
|
"signInWithGoogle": "Sign in with Google",
|
||||||
"signInWithAzureAD": "Sign in with AzureAD",
|
"signInWithAzureAD": "Sign in with AzureAD",
|
||||||
@@ -79,6 +80,13 @@
|
|||||||
"description": "You need to complete your registration by providing your email address and name."
|
"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": {
|
"mfa": {
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Verify your identity",
|
"title": "Verify your identity",
|
||||||
|
@@ -49,6 +49,7 @@
|
|||||||
"idp": {
|
"idp": {
|
||||||
"title": "Iniciar sesión con SSO",
|
"title": "Iniciar sesión con SSO",
|
||||||
"description": "Selecciona uno de los siguientes proveedores para iniciar sesión",
|
"description": "Selecciona uno de los siguientes proveedores para iniciar sesión",
|
||||||
|
"orSignInWith": "o iniciar sesión con",
|
||||||
"signInWithApple": "Iniciar sesión con Apple",
|
"signInWithApple": "Iniciar sesión con Apple",
|
||||||
"signInWithGoogle": "Iniciar sesión con Google",
|
"signInWithGoogle": "Iniciar sesión con Google",
|
||||||
"signInWithAzureAD": "Iniciar sesión con AzureAD",
|
"signInWithAzureAD": "Iniciar sesión con AzureAD",
|
||||||
@@ -79,6 +80,13 @@
|
|||||||
"description": "Para completar el registro, debes establecer una contraseña."
|
"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": {
|
"mfa": {
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Verifica tu identidad",
|
"title": "Verifica tu identidad",
|
||||||
|
@@ -49,6 +49,7 @@
|
|||||||
"idp": {
|
"idp": {
|
||||||
"title": "Accedi con SSO",
|
"title": "Accedi con SSO",
|
||||||
"description": "Seleziona uno dei seguenti provider per accedere",
|
"description": "Seleziona uno dei seguenti provider per accedere",
|
||||||
|
"orSignInWith": "o accedi con",
|
||||||
"signInWithApple": "Accedi con Apple",
|
"signInWithApple": "Accedi con Apple",
|
||||||
"signInWithGoogle": "Accedi con Google",
|
"signInWithGoogle": "Accedi con Google",
|
||||||
"signInWithAzureAD": "Accedi con AzureAD",
|
"signInWithAzureAD": "Accedi con AzureAD",
|
||||||
@@ -79,6 +80,13 @@
|
|||||||
"description": "Completa la registrazione del tuo account."
|
"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": {
|
"mfa": {
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Verifica la tua identità",
|
"title": "Verifica la tua identità",
|
||||||
|
@@ -49,6 +49,7 @@
|
|||||||
"idp": {
|
"idp": {
|
||||||
"title": "Zaloguj się za pomocą SSO",
|
"title": "Zaloguj się za pomocą SSO",
|
||||||
"description": "Wybierz jednego z poniższych dostawców, aby się zalogować",
|
"description": "Wybierz jednego z poniższych dostawców, aby się zalogować",
|
||||||
|
"orSignInWith": "lub zaloguj się przez",
|
||||||
"signInWithApple": "Zaloguj się przez Apple",
|
"signInWithApple": "Zaloguj się przez Apple",
|
||||||
"signInWithGoogle": "Zaloguj się przez Google",
|
"signInWithGoogle": "Zaloguj się przez Google",
|
||||||
"signInWithAzureAD": "Zaloguj się przez AzureAD",
|
"signInWithAzureAD": "Zaloguj się przez AzureAD",
|
||||||
@@ -79,6 +80,13 @@
|
|||||||
"description": "Ukończ rejestrację swojego konta."
|
"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": {
|
"mfa": {
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Zweryfikuj swoją tożsamość",
|
"title": "Zweryfikuj swoją tożsamość",
|
||||||
|
@@ -49,6 +49,7 @@
|
|||||||
"idp": {
|
"idp": {
|
||||||
"title": "Войти через SSO",
|
"title": "Войти через SSO",
|
||||||
"description": "Выберите одного из провайдеров для входа",
|
"description": "Выберите одного из провайдеров для входа",
|
||||||
|
"orSignInWith": "или войти через",
|
||||||
"signInWithApple": "Войти через Apple",
|
"signInWithApple": "Войти через Apple",
|
||||||
"signInWithGoogle": "Войти через Google",
|
"signInWithGoogle": "Войти через Google",
|
||||||
"signInWithAzureAD": "Войти через AzureAD",
|
"signInWithAzureAD": "Войти через AzureAD",
|
||||||
@@ -79,6 +80,13 @@
|
|||||||
"description": "Завершите регистрацию вашего аккаунта."
|
"description": "Завершите регистрацию вашего аккаунта."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ldap": {
|
||||||
|
"title": "Войти через LDAP",
|
||||||
|
"description": "Введите ваши учетные данные LDAP.",
|
||||||
|
"username": "Имя пользователя",
|
||||||
|
"password": "Пароль",
|
||||||
|
"submit": "Продолжить"
|
||||||
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Подтвердите вашу личность",
|
"title": "Подтвердите вашу личность",
|
||||||
|
@@ -49,6 +49,7 @@
|
|||||||
"idp": {
|
"idp": {
|
||||||
"title": "使用 SSO 登录",
|
"title": "使用 SSO 登录",
|
||||||
"description": "选择以下提供商中的一个进行登录",
|
"description": "选择以下提供商中的一个进行登录",
|
||||||
|
"orSignInWith": "或使用以下方式登录",
|
||||||
"signInWithApple": "用 Apple 登录",
|
"signInWithApple": "用 Apple 登录",
|
||||||
"signInWithGoogle": "用 Google 登录",
|
"signInWithGoogle": "用 Google 登录",
|
||||||
"signInWithAzureAD": "用 AzureAD 登录",
|
"signInWithAzureAD": "用 AzureAD 登录",
|
||||||
@@ -79,6 +80,13 @@
|
|||||||
"description": "完成您的账户注册。"
|
"description": "完成您的账户注册。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ldap": {
|
||||||
|
"title": "使用 LDAP 登录",
|
||||||
|
"description": "请输入您的 LDAP 凭据。",
|
||||||
|
"username": "用户名",
|
||||||
|
"password": "密码",
|
||||||
|
"submit": "继续"
|
||||||
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "验证您的身份",
|
"title": "验证您的身份",
|
||||||
|
56
apps/login/src/app/(login)/idp/ldap/page.tsx
Normal file
56
apps/login/src/app/(login)/idp/ldap/page.tsx
Normal file
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
@@ -76,15 +76,17 @@ export default async function Page(props: {
|
|||||||
suffix={suffix}
|
suffix={suffix}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
allowRegister={!!loginSettings?.allowRegister}
|
allowRegister={!!loginSettings?.allowRegister}
|
||||||
>
|
></UsernameForm>
|
||||||
{identityProviders && loginSettings?.allowExternalIdp && (
|
|
||||||
|
{identityProviders && loginSettings?.allowExternalIdp && (
|
||||||
|
<div className="w-full pt-6 pb-4">
|
||||||
<SignInWithIdp
|
<SignInWithIdp
|
||||||
identityProviders={identityProviders}
|
identityProviders={identityProviders}
|
||||||
requestId={requestId}
|
requestId={requestId}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
></SignInWithIdp>
|
></SignInWithIdp>
|
||||||
)}
|
</div>
|
||||||
</UsernameForm>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
|
@@ -11,7 +11,6 @@ import {
|
|||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
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";
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
export default async function Page(props: {
|
export default async function Page(props: {
|
||||||
@@ -95,10 +94,6 @@ export default async function Page(props: {
|
|||||||
requestId={requestId}
|
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
|
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}
|
loginSettings={loginSettings}
|
||||||
promptPasswordless={
|
|
||||||
loginSettings?.passkeysType == PasskeysType.ALLOWED
|
|
||||||
}
|
|
||||||
isAlternative={alt === "true"}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -24,6 +24,7 @@ import {
|
|||||||
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||||
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
|
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_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 { headers } from "next/headers";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { DEFAULT_CSP } from "../../../constants/csp";
|
import { DEFAULT_CSP } from "../../../constants/csp";
|
||||||
@@ -191,6 +192,19 @@ export async function GET(request: NextRequest) {
|
|||||||
const origin = request.nextUrl.origin;
|
const origin = request.nextUrl.origin;
|
||||||
|
|
||||||
const identityProviderType = identityProviders[0].type;
|
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);
|
let provider = idpTypeToSlug(identityProviderType);
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
109
apps/login/src/components/ldap-username-password-form.tsx
Normal file
109
apps/login/src/components/ldap-username-password-form.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"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";
|
||||||
|
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 t = useTranslations("ldap");
|
||||||
|
|
||||||
|
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={t("username")}
|
||||||
|
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={t("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="submit" namespace="ldap" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
@@ -208,26 +208,26 @@ export function LoginPasskey({
|
|||||||
type="button"
|
type="button"
|
||||||
variant={ButtonVariants.Secondary}
|
variant={ButtonVariants.Secondary}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const params: any = { alt: "true" };
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (loginName) {
|
if (loginName) {
|
||||||
params.loginName = loginName;
|
params.append("loginName", loginName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
params.sessionId = sessionId;
|
params.append("sessionId", sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestId) {
|
if (requestId) {
|
||||||
params.requestId = requestId;
|
params.append("requestId", requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
params.organization = organization;
|
params.append("organization", organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
return router.push(
|
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"
|
data-testid="password-button"
|
||||||
|
@@ -23,8 +23,6 @@ type Props = {
|
|||||||
loginName: string;
|
loginName: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
isAlternative?: boolean; // whether password was requested as alternative auth method
|
|
||||||
promptPasswordless?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PasswordForm({
|
export function PasswordForm({
|
||||||
@@ -32,8 +30,6 @@ export function PasswordForm({
|
|||||||
loginName,
|
loginName,
|
||||||
organization,
|
organization,
|
||||||
requestId,
|
requestId,
|
||||||
promptPasswordless,
|
|
||||||
isAlternative,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { register, handleSubmit, formState } = useForm<Inputs>({
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
|
@@ -15,6 +15,7 @@ import { SignInWithGeneric } from "./idps/sign-in-with-generic";
|
|||||||
import { SignInWithGithub } from "./idps/sign-in-with-github";
|
import { SignInWithGithub } from "./idps/sign-in-with-github";
|
||||||
import { SignInWithGitlab } from "./idps/sign-in-with-gitlab";
|
import { SignInWithGitlab } from "./idps/sign-in-with-gitlab";
|
||||||
import { SignInWithGoogle } from "./idps/sign-in-with-google";
|
import { SignInWithGoogle } from "./idps/sign-in-with-google";
|
||||||
|
import { Translated } from "./translated";
|
||||||
|
|
||||||
export interface SignInWithIDPProps {
|
export interface SignInWithIDPProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@@ -53,6 +54,7 @@ export function SignInWithIdp({
|
|||||||
[IdentityProviderType.GITLAB]: SignInWithGitlab,
|
[IdentityProviderType.GITLAB]: SignInWithGitlab,
|
||||||
[IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab,
|
[IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab,
|
||||||
[IdentityProviderType.SAML]: SignInWithGeneric,
|
[IdentityProviderType.SAML]: SignInWithGeneric,
|
||||||
|
[IdentityProviderType.LDAP]: SignInWithGeneric,
|
||||||
[IdentityProviderType.JWT]: SignInWithGeneric,
|
[IdentityProviderType.JWT]: SignInWithGeneric,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,6 +77,9 @@ export function SignInWithIdp({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full space-y-2 text-sm">
|
<div className="flex flex-col w-full space-y-2 text-sm">
|
||||||
|
<p className="text-center ztdl-p">
|
||||||
|
<Translated i18nKey="orSignInWith" namespace="idp" />
|
||||||
|
</p>
|
||||||
{!!identityProviders.length && identityProviders?.map(renderIDPButton)}
|
{!!identityProviders.length && identityProviders?.map(renderIDPButton)}
|
||||||
{state?.error && (
|
{state?.error && (
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
|
@@ -136,9 +136,6 @@ export function UsernameForm({
|
|||||||
<Alert>{error}</Alert>
|
<Alert>{error}</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="pt-6 pb-4">{children}</div>
|
|
||||||
|
|
||||||
<div className="mt-4 flex w-full flex-row items-center">
|
<div className="mt-4 flex w-full flex-row items-center">
|
||||||
<BackButton data-testid="back-button" />
|
<BackButton data-testid="back-button" />
|
||||||
<span className="flex-grow"></span>
|
<span className="flex-grow"></span>
|
||||||
|
@@ -24,6 +24,8 @@ export function idpTypeToSlug(idpType: IdentityProviderType) {
|
|||||||
return "oauth";
|
return "oauth";
|
||||||
case IdentityProviderType.OIDC:
|
case IdentityProviderType.OIDC:
|
||||||
return "oidc";
|
return "oidc";
|
||||||
|
case IdentityProviderType.LDAP:
|
||||||
|
return "ldap";
|
||||||
case IdentityProviderType.JWT:
|
case IdentityProviderType.JWT:
|
||||||
return "jwt";
|
return "jwt";
|
||||||
default:
|
default:
|
||||||
|
@@ -4,6 +4,7 @@ import {
|
|||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getUserByID,
|
getUserByID,
|
||||||
startIdentityProviderFlow,
|
startIdentityProviderFlow,
|
||||||
|
startLDAPIdentityProviderFlow,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
@@ -18,6 +19,13 @@ export async function redirectToIdp(
|
|||||||
prevState: RedirectToIdpState,
|
prevState: RedirectToIdpState,
|
||||||
formData: FormData,
|
formData: FormData,
|
||||||
): Promise<RedirectToIdpState> {
|
): Promise<RedirectToIdpState> {
|
||||||
|
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 params = new URLSearchParams();
|
||||||
|
|
||||||
const linkOnly = formData.get("linkOnly") === "true";
|
const linkOnly = formData.get("linkOnly") === "true";
|
||||||
@@ -30,44 +38,48 @@ export async function redirectToIdp(
|
|||||||
if (requestId) params.set("requestId", requestId);
|
if (requestId) params.set("requestId", requestId);
|
||||||
if (organization) params.set("organization", organization);
|
if (organization) params.set("organization", organization);
|
||||||
|
|
||||||
|
// redirect to LDAP page where username and password is requested
|
||||||
|
if (provider === "ldap") {
|
||||||
|
params.set("idpId", idpId);
|
||||||
|
redirect(`/idp/ldap?` + params.toString());
|
||||||
|
}
|
||||||
|
|
||||||
const response = await startIDPFlow({
|
const response = await startIDPFlow({
|
||||||
|
serviceUrl,
|
||||||
|
host,
|
||||||
idpId,
|
idpId,
|
||||||
successUrl: `/idp/${provider}/success?` + params.toString(),
|
successUrl: `/idp/${provider}/success?` + params.toString(),
|
||||||
failureUrl: `/idp/${provider}/failure?` + params.toString(),
|
failureUrl: `/idp/${provider}/failure?` + params.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && "error" in response && response?.error) {
|
if (!response) {
|
||||||
return { error: response.error };
|
return { error: "Could not start IDP flow" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && "redirect" in response && response?.redirect) {
|
if (response && "redirect" in response && response?.redirect) {
|
||||||
redirect(response.redirect);
|
redirect(response.redirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { error: "Unexpected response from IDP flow" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StartIDPFlowCommand = {
|
export type StartIDPFlowCommand = {
|
||||||
|
serviceUrl: string;
|
||||||
|
host: string;
|
||||||
idpId: string;
|
idpId: string;
|
||||||
successUrl: string;
|
successUrl: string;
|
||||||
failureUrl: string;
|
failureUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function startIDPFlow(command: StartIDPFlowCommand) {
|
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" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||||
|
|
||||||
return startIdentityProviderFlow({
|
return startIdentityProviderFlow({
|
||||||
serviceUrl,
|
serviceUrl: command.serviceUrl,
|
||||||
idpId: command.idpId,
|
idpId: command.idpId,
|
||||||
urls: {
|
urls: {
|
||||||
successUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}${command.successUrl}`,
|
successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`,
|
||||||
failureUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}${command.failureUrl}`,
|
failureUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.failureUrl}`,
|
||||||
},
|
},
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (
|
if (
|
||||||
@@ -174,3 +186,58 @@ export async function createNewSessionFromIdpIntent(
|
|||||||
return { redirect: url };
|
return { redirect: url };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type createNewSessionForLDAPCommand = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
idpId: string;
|
||||||
|
link: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
userId,
|
||||||
|
id: idpIntentId,
|
||||||
|
token: idpIntentToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (command.link) {
|
||||||
|
params.set("link", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirect: `/idp/ldap/success?` + params.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -979,6 +979,34 @@ export async function startIdentityProviderFlow({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function startLDAPIdentityProviderFlow({
|
||||||
|
serviceUrl,
|
||||||
|
idpId,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
idpId: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}) {
|
||||||
|
const userService: Client<typeof UserService> = await createServiceForHost(
|
||||||
|
UserService,
|
||||||
|
serviceUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
return userService.startIdentityProviderIntent({
|
||||||
|
idpId,
|
||||||
|
content: {
|
||||||
|
case: "ldap",
|
||||||
|
value: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAuthRequest({
|
export async function getAuthRequest({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
import { SecuritySettings } from "@zitadel/proto/zitadel/settings/v2/security_settings_pb";
|
||||||
|
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { DEFAULT_CSP } from "../constants/csp";
|
import { DEFAULT_CSP } from "../constants/csp";
|
||||||
import { getServiceUrlFromHeaders } from "./lib/service-url";
|
import { getServiceUrlFromHeaders } from "./lib/service-url";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
"/.well-known/:path*",
|
"/.well-known/:path*",
|
||||||
@@ -14,6 +15,29 @@ export const config = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function loadSecuritySettings(
|
||||||
|
request: NextRequest,
|
||||||
|
): Promise<SecuritySettings | null> {
|
||||||
|
const securityResponse = await fetch(`${request.nextUrl.origin}/security`);
|
||||||
|
|
||||||
|
if (!securityResponse.ok) {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch security settings:",
|
||||||
|
securityResponse.statusText,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await securityResponse.json();
|
||||||
|
|
||||||
|
if (!response || !response.settings) {
|
||||||
|
console.error("No security settings found in the response.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.settings;
|
||||||
|
}
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
// Add the original URL as a header to all requests
|
// Add the original URL as a header to all requests
|
||||||
const requestHeaders = new Headers(request.headers);
|
const requestHeaders = new Headers(request.headers);
|
||||||
@@ -25,7 +49,7 @@ export async function middleware(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only run the rest of the logic for the original matcher paths
|
// Only run the rest of the logic for the original matcher paths
|
||||||
const matchedPaths = [
|
const proxyPaths = [
|
||||||
"/.well-known/",
|
"/.well-known/",
|
||||||
"/oauth/",
|
"/oauth/",
|
||||||
"/oidc/",
|
"/oidc/",
|
||||||
@@ -33,19 +57,17 @@ export async function middleware(request: NextRequest) {
|
|||||||
"/saml/",
|
"/saml/",
|
||||||
];
|
];
|
||||||
|
|
||||||
const isMatched = matchedPaths.some((prefix) =>
|
const isMatched = proxyPaths.some((prefix) =>
|
||||||
request.nextUrl.pathname.startsWith(prefix),
|
request.nextUrl.pathname.startsWith(prefix),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isMatched) {
|
|
||||||
// For all other routes, just add the header and continue
|
|
||||||
return NextResponse.next({
|
|
||||||
request: { headers: requestHeaders },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// escape proxy if the environment is setup for multitenancy
|
// escape proxy if the environment is setup for multitenancy
|
||||||
if (!process.env.ZITADEL_API_URL || !process.env.ZITADEL_SERVICE_USER_TOKEN) {
|
if (
|
||||||
|
!isMatched ||
|
||||||
|
!process.env.ZITADEL_API_URL ||
|
||||||
|
!process.env.ZITADEL_SERVICE_USER_TOKEN
|
||||||
|
) {
|
||||||
|
// For all other routes, just add the header and continue
|
||||||
return NextResponse.next({
|
return NextResponse.next({
|
||||||
request: { headers: requestHeaders },
|
request: { headers: requestHeaders },
|
||||||
});
|
});
|
||||||
@@ -54,21 +76,6 @@ export async function middleware(request: NextRequest) {
|
|||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
// Call the /security route handler
|
|
||||||
const securityResponse = await fetch(`${request.nextUrl.origin}/security`);
|
|
||||||
|
|
||||||
if (!securityResponse.ok) {
|
|
||||||
console.error(
|
|
||||||
"Failed to fetch security settings:",
|
|
||||||
securityResponse.statusText,
|
|
||||||
);
|
|
||||||
return NextResponse.next({
|
|
||||||
request: { headers: requestHeaders },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { settings: securitySettings } = await securityResponse.json();
|
|
||||||
|
|
||||||
const instanceHost = `${serviceUrl}`
|
const instanceHost = `${serviceUrl}`
|
||||||
.replace("https://", "")
|
.replace("https://", "")
|
||||||
.replace("http://", "");
|
.replace("http://", "");
|
||||||
@@ -81,6 +88,8 @@ export async function middleware(request: NextRequest) {
|
|||||||
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
||||||
responseHeaders.set("Access-Control-Allow-Headers", "*");
|
responseHeaders.set("Access-Control-Allow-Headers", "*");
|
||||||
|
|
||||||
|
const securitySettings = await loadSecuritySettings(request);
|
||||||
|
|
||||||
if (securitySettings?.embeddedIframe?.enabled) {
|
if (securitySettings?.embeddedIframe?.enabled) {
|
||||||
responseHeaders.set(
|
responseHeaders.set(
|
||||||
"Content-Security-Policy",
|
"Content-Security-Policy",
|
||||||
|
Reference in New Issue
Block a user