import { DynamicTheme } from "@/components/dynamic-theme"; import { IdpSignin } from "@/components/idp-signin"; import { linkingFailed } from "@/components/idps/pages/linking-failed"; import { linkingSuccess } from "@/components/idps/pages/linking-success"; import { loginFailed } from "@/components/idps/pages/login-failed"; import { loginSuccess } from "@/components/idps/pages/login-success"; import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp"; import { getApiUrlOfHeaders } from "@/lib/service"; import { addHuman, addIDPLink, getBrandingSettings, getIDPByID, getLoginSettings, getOrgsByDomain, listUsers, retrieveIDPIntent, } from "@/lib/zitadel"; import { 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 { AddHumanUserRequest, AddHumanUserRequestSchema, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; export default async function Page(props: { searchParams: Promise>; params: Promise<{ provider: string }>; }) { const params = await props.params; const searchParams = await props.searchParams; const locale = getLocale(); const t = await getTranslations({ locale, namespace: "idp" }); const { id, token, authRequestId, organization, link } = searchParams; const { provider } = params; const _headers = await headers(); const instanceUrl = getApiUrlOfHeaders(_headers); const host = instanceUrl; if (!host || typeof host !== "string") { throw new Error("No host found"); } const branding = await getBrandingSettings({ host, organization }); if (!provider || !id || !token) { return loginFailed(branding, "IDP context missing"); } const intent = await retrieveIDPIntent({ serviceUrl: instanceUrl, id, token, }); const { idpInformation, userId } = intent; // sign in user. If user should be linked continue if (userId && !link) { // TODO: update user if idp.options.isAutoUpdate is true return loginSuccess( userId, { idpIntentId: id, idpIntentToken: token }, authRequestId, branding, ); } if (!idpInformation) { return loginFailed(branding, "IDP information missing"); } const idp = await getIDPByID({ host, id: idpInformation.idpId }); const options = idp?.config?.options; if (!idp) { throw new Error("IDP not found"); } const providerType = idpTypeToIdentityProviderType(idp.type); if (link) { if (!options?.isLinkingAllowed) { // linking was probably disallowed since the invitation was created return linkingFailed(branding, "Linking is no longer allowed"); } let idpLink; try { idpLink = await addIDPLink({ host, idp: { id: idpInformation.idpId, userId: idpInformation.userId, userName: idpInformation.userName, }, userId, }); } catch (error) { console.error(error); return linkingFailed(branding); } if (!idpLink) { return linkingFailed(branding); } else { return linkingSuccess( userId, { idpIntentId: id, idpIntentToken: token }, authRequestId, branding, ); } } // search for potential user via username, then link if (options?.isLinkingAllowed) { let foundUser; const email = PROVIDER_MAPPING[providerType](idpInformation).email?.email; if (options.autoLinking === AutoLinkingOption.EMAIL && email) { foundUser = await listUsers({ host, email }).then((response) => { return response.result ? response.result[0] : null; }); } else if (options.autoLinking === AutoLinkingOption.USERNAME) { foundUser = await listUsers( options.autoLinking === AutoLinkingOption.USERNAME ? { host, userName: idpInformation.userName } : { host, email }, ).then((response) => { return response.result ? response.result[0] : null; }); } else { foundUser = await listUsers({ host, userName: idpInformation.userName, email, }).then((response) => { return response.result ? response.result[0] : null; }); } if (foundUser) { let idpLink; try { idpLink = await addIDPLink({ host, idp: { id: idpInformation.idpId, userId: idpInformation.userId, userName: idpInformation.userName, }, userId: foundUser.userId, }); } catch (error) { console.error(error); return linkingFailed(branding); } if (!idpLink) { return linkingFailed(branding); } else { return linkingSuccess( foundUser.userId, { idpIntentId: id, idpIntentToken: token }, authRequestId, branding, ); } } } if (options?.isCreationAllowed && options.isAutoCreation) { let orgToRegisterOn: string | undefined = organization; let userData: AddHumanUserRequest = PROVIDER_MAPPING[providerType](idpInformation); if ( !orgToRegisterOn && userData.username && // username or email? ORG_SUFFIX_REGEX.test(userData.username) ) { const matched = ORG_SUFFIX_REGEX.exec(userData.username); const suffix = matched?.[1] ?? ""; // this just returns orgs where the suffix is set as primary domain const orgs = await getOrgsByDomain({ host, domain: suffix }); const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; const orgLoginSettings = await getLoginSettings({ host, organization: orgToCheckForDiscovery, }); if (orgLoginSettings?.allowDomainDiscovery) { orgToRegisterOn = orgToCheckForDiscovery; } } if (orgToRegisterOn) { const organizationSchema = create(OrganizationSchema, { org: { case: "orgId", value: orgToRegisterOn }, }); userData = create(AddHumanUserRequestSchema, { ...userData, organization: organizationSchema, }); } const newUser = await addHuman({ host, request: userData }); if (newUser) { return (

{t("registerSuccess.title")}

{t("registerSuccess.description")}

); } } // return login failed if no linking or creation is allowed and no user was found return loginFailed(branding, "No user found"); }