mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 16:17:32 +00:00
ldap page, start idp flow
This commit is contained in:
@@ -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">
|
||||||
|
55
apps/login/src/app/(login)/idp/ldap/page.tsx
Normal file
55
apps/login/src/app/(login)/idp/ldap/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -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"
|
||||||
|
@@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user