diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index cf5f0c8637..3e4a7b253a 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -1,5 +1,6 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { IdpSignin } from "@/components/idp-signin"; +import { completeIDP } from "@/components/idps/pages/complete-idp"; import { linkingFailed } from "@/components/idps/pages/linking-failed"; import { linkingSuccess } from "@/components/idps/pages/linking-success"; import { loginFailed } from "@/components/idps/pages/login-failed"; @@ -24,7 +25,6 @@ import { } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; -import { redirect } from "next/navigation"; const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; @@ -177,9 +177,10 @@ export default async function Page(props: { } } - if (options?.isAutoCreation) { + let newUser; + // automatic creation of a user is allowed and data is complete + if (options?.isAutoCreation && addHumanUser) { let orgToRegisterOn: string | undefined = organization; - let newUser; if ( !orgToRegisterOn && @@ -206,70 +207,68 @@ export default async function Page(props: { } } - // if addHumanUser is provided in the intent, expect that it can be created otherwise show an error - if (addHumanUser) { - let addHumanUserWithOrganization: AddHumanUserRequest; - if (orgToRegisterOn) { - const organizationSchema = create(OrganizationSchema, { - org: { case: "orgId", value: orgToRegisterOn }, - }); + let addHumanUserWithOrganization: AddHumanUserRequest; + if (orgToRegisterOn) { + const organizationSchema = create(OrganizationSchema, { + org: { case: "orgId", value: orgToRegisterOn }, + }); - addHumanUserWithOrganization = create(AddHumanUserRequestSchema, { - ...addHumanUser, - organization: organizationSchema, - }); - } else { - addHumanUserWithOrganization = create( - AddHumanUserRequestSchema, - addHumanUser, - ); - } - - try { - newUser = await addHuman({ - serviceUrl, - request: addHumanUserWithOrganization, - }); - } catch (error: unknown) { - console.error( - "An error occurred while creating the user:", - error, - addHumanUser, - ); - return loginFailed( - branding, - (error as ConnectError).message - ? (error as ConnectError).message - : "Could not create user", - ); - } + addHumanUserWithOrganization = create(AddHumanUserRequestSchema, { + ...addHumanUser, + organization: organizationSchema, + }); } else { - // if no user was found, we will create a new user manually / redirect to the registration page - if (options.isCreationAllowed) { - const registerParams = new URLSearchParams({ - idpIntentId: id, - idpIntentToken: token, - organization: organization ?? "", - }); - return redirect(`/register?${registerParams})}`); - } - } - - if (newUser) { - return ( - - - {t("registerSuccess.title")} - {t("registerSuccess.description")} - - - + addHumanUserWithOrganization = create( + AddHumanUserRequestSchema, + addHumanUser, ); } + + try { + newUser = await addHuman({ + serviceUrl, + request: addHumanUserWithOrganization, + }); + } catch (error: unknown) { + console.error( + "An error occurred while creating the user:", + error, + addHumanUser, + ); + return loginFailed( + branding, + (error as ConnectError).message + ? (error as ConnectError).message + : "Could not create user", + ); + } + } + + // if no user was found, we will create a new user manually / redirect to the registration page + if (options?.isCreationAllowed) { + return completeIDP({ + branding, + idpInformation, + organization, + requestId, + userId, + }); + } + + if (newUser) { + return ( + + + {t("registerSuccess.title")} + {t("registerSuccess.description")} + + + + ); } // return login failed if no linking or creation is allowed and no user was found diff --git a/apps/login/src/components/idps/pages/complete-idp.tsx b/apps/login/src/components/idps/pages/complete-idp.tsx new file mode 100644 index 0000000000..4443a9317b --- /dev/null +++ b/apps/login/src/components/idps/pages/complete-idp.tsx @@ -0,0 +1,38 @@ +import { RegisterFormIDPIncomplete } from "@/components/register-form-idp-incomplete"; +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb"; +import { getLocale, getTranslations } from "next-intl/server"; +import { DynamicTheme } from "../../dynamic-theme"; + +export async function completeIDP({ + userId, + idpInformation, + requestId, + organization, + branding, +}: { + userId: string; + idpInformation: IDPInformation; + requestId?: string; + organization?: string; + branding?: BrandingSettings; +}) { + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "idp" }); + + return ( + + + {t("loginSuccess.title")} + {t("loginSuccess.description")} + + + + + ); +} diff --git a/apps/login/src/components/register-form-idp-incomplete.tsx b/apps/login/src/components/register-form-idp-incomplete.tsx new file mode 100644 index 0000000000..35324be2e2 --- /dev/null +++ b/apps/login/src/components/register-form-idp-incomplete.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { FieldValues, useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { AuthenticationMethod, methods } from "./authentication-method-radio"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; + +type Inputs = + | { + firstname: string; + lastname: string; + email: string; + } + | FieldValues; + +type Props = { + organization?: string; + requestId?: string; + idpInformation?: IDPInformation; +}; + +export function RegisterFormIDPIncomplete({ + organization, + requestId, + idpInformation, +}: Props) { + const t = useTranslations("register"); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + email: idpInformation?.rawInformation?.email ?? "", + firstName: idpInformation?.rawInformation?.firstname ?? "", + lastname: idpInformation?.rawInformation?.lastname ?? "", + }, + }); + + const [loading, setLoading] = useState(false); + const [selected, setSelected] = useState(methods[0]); + const [error, setError] = useState(""); + + const router = useRouter(); + + async function submitAndRegister(values: Inputs) { + setLoading(true); + const response = await registerUserAndLinkToIDP({ + email: values.email, + firstName: values.firstname, + lastName: values.lastname, + organization: organization, + requestId: requestId, + }) + .catch(() => { + setError("Could not register user"); + 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 response; + } + + const { errors } = formState; + + return ( + + + + + + + + + + + + + + {t("completeData")} + + {error && ( + + {error} + + )} + + + + + {loading && } + {t("submit")} + + + + ); +} diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index 88e0b48290..841fc06b3a 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -109,15 +109,20 @@ export async function createSessionAndUpdateCookie(command: { } } -export async function createSessionForIdpAndUpdateCookie( - userId: string, +export async function createSessionForIdpAndUpdateCookie({ + userId, + idpIntent, + requestId, + lifetime, +}: { + userId: string; idpIntent: { idpIntentId?: string | undefined; idpIntentToken?: string | undefined; - }, - requestId: string | undefined, - lifetime?: Duration, -): Promise { + }; + requestId: string | undefined; + lifetime?: Duration; +}): Promise { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index 5cac537690..aaf5b77779 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -122,12 +122,12 @@ export async function createNewSessionFromIdpIntent( organization: userResponse.user.details?.resourceOwner, }); - const session = await createSessionForIdpAndUpdateCookie( - command.userId, - command.idpIntent, - command.requestId, - loginSettings?.externalLoginCheckLifetime, - ); + const session = await createSessionForIdpAndUpdateCookie({ + userId: command.userId, + idpIntent: command.idpIntent, + requestId: command.requestId, + lifetime: loginSettings?.externalLoginCheckLifetime, + }); if (!session || !session.factors?.user) { return { error: "Could not create session" }; diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts index 25bea33527..b08099817d 100644 --- a/apps/login/src/lib/server/register.ts +++ b/apps/login/src/lib/server/register.ts @@ -1,6 +1,9 @@ "use server"; -import { createSessionAndUpdateCookie } from "@/lib/server/cookie"; +import { + createSessionAndUpdateCookie, + createSessionForIdpAndUpdateCookie, +} from "@/lib/server/cookie"; import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel"; import { create } from "@zitadel/client"; import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; @@ -133,3 +136,79 @@ export async function registerUser(command: RegisterUserCommand) { return { redirect: url }; } } + +type RegisterUserAndLinkToIDPommand = { + email: string; + firstName: string; + lastName: string; + organization?: string; + requestId?: string; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; + userId: string; +}; + +export type registerUserAndLinkToIDPResponse = { + userId: string; + sessionId: string; + factors: Factors | undefined; +}; +export async function registerUserAndLinkToIDP( + command: RegisterUserAndLinkToIDPommand, +) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + const addResponse = await addHumanUser({ + serviceUrl, + email: command.email, + firstName: command.firstName, + lastName: command.lastName, + organization: command.organization, + }); + + if (!addResponse) { + return { error: "Could not create user" }; + } + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: command.organization, + }); + + // TODO: addIDPLink to addResponse + + const session = await createSessionForIdpAndUpdateCookie({ + requestId: command.requestId, + userId: command.userId, + idpIntent: command.idpIntent, + lifetime: loginSettings?.externalLoginCheckLifetime, + }); + + if (!session || !session.factors?.user) { + return { error: "Could not create session" }; + } + + const url = await getNextUrl( + command.requestId && session.id + ? { + sessionId: session.id, + requestId: command.requestId, + organization: session.factors.user.organizationId, + } + : { + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; +} diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index a0e91a021c..c53d622d7d 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -387,7 +387,7 @@ export type AddHumanUserData = { firstName: string; lastName: string; email: string; - password: string | undefined; + password?: string; organization: string | undefined; };
{t("registerSuccess.description")}
{t("loginSuccess.description")}
{t("completeData")}