ldap page, start idp flow

This commit is contained in:
Max Peintner
2025-05-28 10:56:46 +02:00
parent 83a24cec46
commit 7ea2103b57
5 changed files with 167 additions and 32 deletions

View File

@@ -184,7 +184,7 @@ export default async function Page(props: {
></ChooseAuthenticatorToSetup> ></ChooseAuthenticatorToSetup>
)} )}
{loginSettings?.allowExternalIdp && identityProviders && ( {loginSettings?.allowExternalIdp && !!identityProviders.length && (
<> <>
{identityProviders.length && ( {identityProviders.length && (
<div className="py-3 flex flex-col"> <div className="py-3 flex flex-col">

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
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 (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1>
<p className="ztdl-p">{t("description")}</p>
<UsernamePasswordForm
idpId={idpId}
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
></UsernamePasswordForm>
</div>
</DynamicTheme>
);
}

View File

@@ -1,9 +1,6 @@
"use client"; "use client";
import { sendPassword } from "@/lib/server/password"; import { createNewSessionForLDAP } from "@/lib/server/idp";
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 { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@@ -20,17 +17,15 @@ type Inputs = {
}; };
type Props = { type Props = {
loginSettings: LoginSettings | undefined;
loginName: string;
organization?: string; organization?: string;
requestId?: string; requestId?: string;
idpId: string;
}; };
export function UsernamePasswordForm({ export function UsernamePasswordForm({
loginSettings,
loginName,
organization, organization,
requestId, requestId,
idpId,
}: Props) { }: Props) {
const t = useTranslations("password"); const t = useTranslations("password");
@@ -48,13 +43,10 @@ export function UsernamePasswordForm({
setError(""); setError("");
setLoading(true); setLoading(true);
const response = await sendPassword({ const response = await createNewSessionForLDAP({
loginName, idpId: idpId,
organization, username: values.loginName,
checks: create(ChecksSchema, { password: values.password,
password: { password: values.password },
}),
requestId,
}) })
.catch(() => { .catch(() => {
setError("Could not verify password"); setError("Could not verify password");
@@ -75,7 +67,7 @@ export function UsernamePasswordForm({
} }
return ( return (
<form className="w-full"> <form className="w-full space-y-4">
<TextInput <TextInput
type="text" type="text"
autoComplete="username" autoComplete="username"

View File

@@ -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";
@@ -26,52 +34,54 @@ export async function redirectToIdp(
const idpId = formData.get("id") as string; const idpId = formData.get("id") as string;
const provider = formData.get("provider") 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 (linkOnly) params.set("link", "true");
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") { if (provider === "ldap") {
redirect("/idp/ldap?linkOnly=" + linkOnly + "&" + params.toString()); 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 (
@@ -178,3 +188,53 @@ export async function createNewSessionFromIdpIntent(
return { redirect: url }; 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(),
};
}

View File

@@ -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<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,