From f31fac4c9ea4fe8e41f77259bc3f5c5dd82d5221 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 11 Jun 2025 13:38:38 +0200 Subject: [PATCH] link user to idp after creation --- .../(login)/idp/[provider]/success/page.tsx | 96 +++++++++++++------ apps/login/src/app/(login)/register/page.tsx | 28 ++---- .../components/idps/pages/complete-idp.tsx | 26 +++-- .../register-form-idp-incomplete.tsx | 29 ++++-- apps/login/src/components/register-form.tsx | 2 +- apps/login/src/lib/server/invite.ts | 2 +- apps/login/src/lib/server/register.ts | 31 +++++- apps/login/src/lib/zitadel.ts | 51 ++++++---- 8 files changed, 171 insertions(+), 94 deletions(-) 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 c01adcf346..a31aed4bb3 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -10,6 +10,7 @@ import { addHuman, addIDPLink, getBrandingSettings, + getDefaultOrg, getIDPByID, getLoginSettings, getOrgsByDomain, @@ -19,6 +20,7 @@ import { import { ConnectError, create } from "@zitadel/client"; import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { AddHumanUserRequest, AddHumanUserRequestSchema, @@ -28,6 +30,41 @@ import { headers } from "next/headers"; const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; +async function resolveOrganizationForUser({ + organization, + addHumanUser, + serviceUrl, +}: { + organization?: string; + addHumanUser?: { username?: string }; + serviceUrl: string; +}): Promise { + if (organization) return organization; + + if (addHumanUser?.username && ORG_SUFFIX_REGEX.test(addHumanUser.username)) { + const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username); + const suffix = matched?.[1] ?? ""; + + const orgs = await getOrgsByDomain({ + serviceUrl, + domain: suffix, + }); + const orgToCheckForDiscovery = + orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; + + if (orgToCheckForDiscovery) { + const orgLoginSettings = await getLoginSettings({ + serviceUrl, + organization: orgToCheckForDiscovery, + }); + if (orgLoginSettings?.allowDomainDiscovery) { + return orgToCheckForDiscovery; + } + } + } + return undefined; +} + export default async function Page(props: { searchParams: Promise>; params: Promise<{ provider: string }>; @@ -36,7 +73,7 @@ export default async function Page(props: { const searchParams = await props.searchParams; const locale = getLocale(); const t = await getTranslations({ locale, namespace: "idp" }); - const { id, token, requestId, organization, link } = searchParams; + let { id, token, requestId, organization, link } = searchParams; const { provider } = params; const _headers = await headers(); @@ -47,6 +84,15 @@ export default async function Page(props: { organization, }); + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + organization = org.id; + } + } + if (!provider || !id || !token) { return loginFailed(branding, "IDP context missing"); } @@ -180,32 +226,11 @@ export default async function Page(props: { let newUser; // automatic creation of a user is allowed and data is complete if (options?.isAutoCreation && addHumanUser) { - let orgToRegisterOn: string | undefined = organization; - - if ( - !orgToRegisterOn && - addHumanUser?.username && // username or email? - ORG_SUFFIX_REGEX.test(addHumanUser.username) - ) { - const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username); - const suffix = matched?.[1] ?? ""; - - // this just returns orgs where the suffix is set as primary domain - const orgs = await getOrgsByDomain({ - serviceUrl, - domain: suffix, - }); - const orgToCheckForDiscovery = - orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; - - const orgLoginSettings = await getLoginSettings({ - serviceUrl, - organization: orgToCheckForDiscovery, - }); - if (orgLoginSettings?.allowDomainDiscovery) { - orgToRegisterOn = orgToCheckForDiscovery; - } - } + const orgToRegisterOn = await resolveOrganizationForUser({ + organization, + addHumanUser, + serviceUrl, + }); let addHumanUserWithOrganization: AddHumanUserRequest; if (orgToRegisterOn) { @@ -244,14 +269,25 @@ export default async function Page(props: { } } else if (options?.isCreationAllowed) { // if no user was found, we will create a new user manually / redirect to the registration page + const orgToRegisterOn = await resolveOrganizationForUser({ + organization, + addHumanUser, + serviceUrl, + }); + + if (!orgToRegisterOn) { + return loginFailed(branding, "No organization found for registration"); + } return completeIDP({ branding, idpIntent: { idpIntentId: id, idpIntentToken: token }, - idpInformation, - organization, + addHumanUser, + organization: orgToRegisterOn, requestId, - userId, + idpUserId: idpInformation?.userId, + idpId: idpInformation?.idpId, + idpUserName: idpInformation?.userName, }); } diff --git a/apps/login/src/app/(login)/register/page.tsx b/apps/login/src/app/(login)/register/page.tsx index 550a214102..3a61dbae7c 100644 --- a/apps/login/src/app/(login)/register/page.tsx +++ b/apps/login/src/app/(login)/register/page.tsx @@ -1,3 +1,4 @@ +import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { RegisterForm } from "@/components/register-form"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; @@ -9,7 +10,6 @@ import { getLegalAndSupportSettings, getLoginSettings, getPasswordComplexitySettings, - retrieveIDPIntent, } from "@/lib/zitadel"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { getLocale, getTranslations } from "next-intl/server"; @@ -21,16 +21,9 @@ export default async function Page(props: { const searchParams = await props.searchParams; const locale = getLocale(); const t = await getTranslations({ locale, namespace: "register" }); + const tError = await getTranslations({ locale, namespace: "error" }); - let { - firstname, - lastname, - email, - organization, - requestId, - idpIntentId, - idpIntentToken, - } = searchParams; + let { firstname, lastname, email, organization, requestId } = searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -44,17 +37,6 @@ export default async function Page(props: { } } - let idpIntent; - if (idpIntentId && idpIntentToken) { - idpIntent = await retrieveIDPIntent({ - serviceUrl, - id: idpIntentId, - token: idpIntentToken, - }); - - const { idpInformation, userId } = idpIntent; - } - const legal = await getLegalAndSupportSettings({ serviceUrl, organization, @@ -100,7 +82,9 @@ export default async function Page(props: {

{t("title")}

{t("description")}

- {legal && passwordComplexitySettings && ( + {!organization && {tError("unknownContext")}} + + {legal && passwordComplexitySettings && organization && ( {t("completeRegister.description")}

({ mode: "onBlur", defaultValues: { - email: idpInformation?.rawInformation?.email ?? "", - firstName: idpInformation?.rawInformation?.firstname ?? "", - lastname: idpInformation?.rawInformation?.lastname ?? "", + email: defaultValues?.email ?? "", + firstname: defaultValues?.firstname ?? "", + lastname: defaultValues?.lastname ?? "", }, }); @@ -57,7 +64,9 @@ export function RegisterFormIDPIncomplete({ async function submitAndRegister(values: Inputs) { setLoading(true); const response = await registerUserAndLinkToIDP({ - userId: userId, + idpId: idpId, + idpUserName: idpUserName, + idpUserId: idpUserId, email: values.email, firstName: values.firstname, lastName: values.lastname, diff --git a/apps/login/src/components/register-form.tsx b/apps/login/src/components/register-form.tsx index 6701fdf215..1999bc7251 100644 --- a/apps/login/src/components/register-form.tsx +++ b/apps/login/src/components/register-form.tsx @@ -35,7 +35,7 @@ type Props = { firstname?: string; lastname?: string; email?: string; - organization?: string; + organization: string; requestId?: string; loginSettings?: LoginSettings; idpCount: number; diff --git a/apps/login/src/lib/server/invite.ts b/apps/login/src/lib/server/invite.ts index c0fc63fef5..40225d9916 100644 --- a/apps/login/src/lib/server/invite.ts +++ b/apps/login/src/lib/server/invite.ts @@ -10,7 +10,7 @@ type InviteUserCommand = { firstName: string; lastName: string; password?: string; - organization?: string; + organization: string; requestId?: string; }; diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts index b08099817d..c753a745b8 100644 --- a/apps/login/src/lib/server/register.ts +++ b/apps/login/src/lib/server/register.ts @@ -4,7 +4,12 @@ import { createSessionAndUpdateCookie, createSessionForIdpAndUpdateCookie, } from "@/lib/server/cookie"; -import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel"; +import { + addHumanUser, + addIDPLink, + getLoginSettings, + getUserByID, +} from "@/lib/zitadel"; import { create } from "@zitadel/client"; import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { @@ -21,7 +26,7 @@ type RegisterUserCommand = { firstName: string; lastName: string; password?: string; - organization?: string; + organization: string; requestId?: string; }; @@ -141,13 +146,15 @@ type RegisterUserAndLinkToIDPommand = { email: string; firstName: string; lastName: string; - organization?: string; + organization: string; requestId?: string; idpIntent: { idpIntentId: string; idpIntentToken: string; }; - userId: string; + idpUserId: string; + idpId: string; + idpUserName: string; }; export type registerUserAndLinkToIDPResponse = { @@ -185,9 +192,23 @@ export async function registerUserAndLinkToIDP( // TODO: addIDPLink to addResponse + const idpLink = await addIDPLink({ + serviceUrl, + idp: { + id: command.idpId, + userId: command.idpUserId, + userName: command.idpUserName, + }, + userId: addResponse.userId, + }); + + if (!idpLink) { + return { error: "Could not link IDP to user" }; + } + const session = await createSessionForIdpAndUpdateCookie({ requestId: command.requestId, - userId: command.userId, + userId: addResponse.userId, // the user we just created idpIntent: command.idpIntent, lifetime: loginSettings?.externalLoginCheckLifetime, }); diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index c53d622d7d..d5045df041 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -1,7 +1,10 @@ import { Client, create, Duration } from "@zitadel/client"; import { makeReqCtx } from "@zitadel/client/v2"; import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb"; -import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb"; +import { + OrganizationSchema, + TextQueryMethod, +} from "@zitadel/proto/zitadel/object/v2/object_pb"; import { CreateCallbackRequest, OIDCService, @@ -32,6 +35,7 @@ import { import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AddHumanUserRequest, + AddHumanUserRequestSchema, ResendEmailCodeRequest, ResendEmailCodeRequestSchema, SendEmailCodeRequestSchema, @@ -388,7 +392,7 @@ export type AddHumanUserData = { lastName: string; email: string; password?: string; - organization: string | undefined; + organization: string; }; export async function addHumanUser({ @@ -404,23 +408,36 @@ export async function addHumanUser({ serviceUrl, ); - return userService.addHumanUser({ - email: { - email, - verification: { - case: "isVerified", - value: false, + let addHumanUserRequest: AddHumanUserRequest = create( + AddHumanUserRequestSchema, + { + email: { + email, + verification: { + case: "isVerified", + value: false, + }, }, + username: email, + profile: { givenName: firstName, familyName: lastName }, + passwordType: password + ? { case: "password", value: { password } } + : undefined, }, - username: email, - profile: { givenName: firstName, familyName: lastName }, - organization: organization - ? { org: { case: "orgId", value: organization } } - : undefined, - passwordType: password - ? { case: "password", value: { password } } - : undefined, - }); + ); + + if (organization) { + const organizationSchema = create(OrganizationSchema, { + org: { case: "orgId", value: organization }, + }); + + addHumanUserRequest = { + ...addHumanUserRequest, + organization: organizationSchema, + }; + } + + return userService.addHumanUser(addHumanUserRequest); } export async function addHuman({