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({