Merge pull request #323 from zitadel/idp-invite

fix: add IDPs to invite flow
This commit is contained in:
Elio Bischof
2024-12-27 12:23:35 +01:00
committed by GitHub
15 changed files with 193 additions and 127 deletions

View File

@@ -183,7 +183,8 @@
"title": "Authentifizierungsmethode auswählen", "title": "Authentifizierungsmethode auswählen",
"description": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten.", "description": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten.",
"noMethodsAvailable": "Keine Authentifizierungsmethoden verfügbar", "noMethodsAvailable": "Keine Authentifizierungsmethoden verfügbar",
"allSetup": "Sie haben bereits einen Authentifikator eingerichtet!" "allSetup": "Sie haben bereits einen Authentifikator eingerichtet!",
"linkWithIDP": "oder verknüpfe mit einem Identitätsanbieter"
}, },
"error": { "error": {
"unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.", "unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.",

View File

@@ -183,7 +183,8 @@
"title": "Choose authentication method", "title": "Choose authentication method",
"description": "Select the method you would like to authenticate", "description": "Select the method you would like to authenticate",
"noMethodsAvailable": "No authentication methods available", "noMethodsAvailable": "No authentication methods available",
"allSetup": "You have already setup an authenticator!" "allSetup": "You have already setup an authenticator!",
"linkWithIDP": "or link with an Identity Provider"
}, },
"error": { "error": {
"unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.", "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.",

View File

@@ -183,7 +183,8 @@
"title": "Seleccionar método de autenticación", "title": "Seleccionar método de autenticación",
"description": "Selecciona el método con el que deseas autenticarte", "description": "Selecciona el método con el que deseas autenticarte",
"noMethodsAvailable": "No hay métodos de autenticación disponibles", "noMethodsAvailable": "No hay métodos de autenticación disponibles",
"allSetup": "¡Ya has configurado un autenticador!" "allSetup": "¡Ya has configurado un autenticador!",
"linkWithIDP": "o vincúlalo con un proveedor de identidad"
}, },
"error": { "error": {
"unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.", "unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.",

View File

@@ -183,7 +183,8 @@
"title": "Seleziona metodo di autenticazione", "title": "Seleziona metodo di autenticazione",
"description": "Seleziona il metodo con cui desideri autenticarti", "description": "Seleziona il metodo con cui desideri autenticarti",
"noMethodsAvailable": "Nessun metodo di autenticazione disponibile", "noMethodsAvailable": "Nessun metodo di autenticazione disponibile",
"allSetup": "Hai già configurato un autenticatore!" "allSetup": "Hai già configurato un autenticatore!",
"linkWithIDP": "o collega con un Identity Provider"
}, },
"error": { "error": {
"unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.", "unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.",

View File

@@ -183,7 +183,8 @@
"title": "选择认证方式", "title": "选择认证方式",
"description": "选择您想使用的认证方法", "description": "选择您想使用的认证方法",
"noMethodsAvailable": "没有可用的认证方法", "noMethodsAvailable": "没有可用的认证方法",
"allSetup": "您已经设置好了一个认证器!" "allSetup": "您已经设置好了一个认证器!",
"linkWithIDP": "或将其与身份提供者关联"
}, },
"error": { "error": {
"unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。", "unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。",

View File

@@ -2,10 +2,12 @@ import { Alert } from "@/components/alert";
import { BackButton } from "@/components/back-button"; import { BackButton } from "@/components/back-button";
import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup"; import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getActiveIdentityProviders,
getBrandingSettings, getBrandingSettings,
getLoginSettings, getLoginSettings,
getSession, getSession,
@@ -86,13 +88,12 @@ export default async function Page(props: {
sessionWithData.factors?.user?.organizationId, sessionWithData.factors?.user?.organizationId,
); );
/* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */ const identityProviders = await getActiveIdentityProviders(
sessionWithData.factors?.user?.organizationId,
// const identityProviders = await getActiveIdentityProviders( true,
// sessionWithData.factors?.user?.organizationId, ).then((resp) => {
// ).then((resp) => { return resp.identityProviders;
// return resp.identityProviders; });
// });
const params = new URLSearchParams({ const params = new URLSearchParams({
initial: "true", // defines that a code is not required and is therefore not shown in the UI initial: "true", // defines that a code is not required and is therefore not shown in the UI
@@ -110,10 +111,6 @@ export default async function Page(props: {
params.set("authRequestId", authRequestId); params.set("authRequestId", authRequestId);
} }
const host = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
@@ -136,21 +133,18 @@ export default async function Page(props: {
></ChooseAuthenticatorToSetup> ></ChooseAuthenticatorToSetup>
)} )}
{/* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */} <div className="py-3 flex flex-col">
<p className="ztdl-p text-center">{t("linkWithIDP")}</p>
{/* <p className="ztdl-p text-center"> </div>
or sign in with an Identity Provider
</p>
{loginSettings?.allowExternalIdp && identityProviders && ( {loginSettings?.allowExternalIdp && identityProviders && (
<SignInWithIdp <SignInWithIdp
host={host}
identityProviders={identityProviders} identityProviders={identityProviders}
authRequestId={authRequestId} authRequestId={authRequestId}
organization={sessionWithData.factors?.user?.organizationId} organization={sessionWithData.factors?.user?.organizationId}
linkOnly={true} // tell the callback function to just link the IDP and not login, to get an error when user is already available linkOnly={true} // tell the callback function to just link the IDP and not login, to get an error when user is already available
></SignInWithIdp> ></SignInWithIdp>
)} */} )}
<div className="mt-8 flex w-full flex-row items-center"> <div className="mt-8 flex w-full flex-row items-center">
<BackButton /> <BackButton />

View File

@@ -28,7 +28,7 @@ export default async function Page(props: {
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("loginError.title")}</h1> <h1>{t("loginError.title")}</h1>
<div>{t("loginError.description")}</div> <p className="ztdl-p">{t("loginError.description")}</p>
</div> </div>
</DynamicTheme> </DynamicTheme>
); );

View File

@@ -1,6 +1,9 @@
import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { IdpSignin } from "@/components/idp-signin"; 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 { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp";
import { import {
addHuman, addHuman,
@@ -15,7 +18,6 @@ import {
import { create } from "@zitadel/client"; import { create } from "@zitadel/client";
import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { import {
AddHumanUserRequest, AddHumanUserRequest,
AddHumanUserRequestSchema, AddHumanUserRequestSchema,
@@ -24,75 +26,6 @@ import { getLocale, getTranslations } from "next-intl/server";
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
async function loginFailed(branding?: BrandingSettings, error: string = "") {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("loginError.title")}</h1>
<p className="ztdl-p">{t("loginError.description")}</p>
{error && (
<div className="w-full">
{<Alert type={AlertType.ALERT}>{error}</Alert>}
</div>
)}
</div>
</DynamicTheme>
);
}
async function loginSuccess(
userId: string,
idpIntent: { idpIntentId: string; idpIntentToken: string },
authRequestId?: string,
branding?: BrandingSettings,
) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("loginSuccess.title")}</h1>
<p className="ztdl-p">{t("loginSuccess.description")}</p>
<IdpSignin
userId={userId}
idpIntent={idpIntent}
authRequestId={authRequestId}
/>
</div>
</DynamicTheme>
);
}
async function linkingSuccess(
userId: string,
idpIntent: { idpIntentId: string; idpIntentToken: string },
authRequestId?: string,
branding?: BrandingSettings,
) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("linkingSuccess.title")}</h1>
<p className="ztdl-p">{t("linkingSuccess.description")}</p>
<IdpSignin
userId={userId}
idpIntent={idpIntent}
authRequestId={authRequestId}
/>
</div>
</DynamicTheme>
);
}
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
params: Promise<{ provider: string }>; params: Promise<{ provider: string }>;
@@ -139,6 +72,39 @@ export default async function Page(props: {
const providerType = idpTypeToIdentityProviderType(idp.type); 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(
{
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 // search for potential user via username, then link
if (options?.isLinkingAllowed) { if (options?.isLinkingAllowed) {
let foundUser; let foundUser;
@@ -166,31 +132,24 @@ export default async function Page(props: {
} }
if (foundUser) { if (foundUser) {
const idpLink = await addIDPLink( let idpLink;
try {
idpLink = await addIDPLink(
{ {
id: idpInformation.idpId, id: idpInformation.idpId,
userId: idpInformation.userId, userId: idpInformation.userId,
userName: idpInformation.userName, userName: idpInformation.userName,
}, },
foundUser.userId, foundUser.userId,
).catch((error) => {
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("linkingError.title")}</h1>
<div className="w-full">
{
<Alert type={AlertType.ALERT}>
{t("linkingError.description")}
</Alert>
}
</div>
</div>
</DynamicTheme>
); );
}); } catch (error) {
console.error(error);
return linkingFailed(branding);
}
if (idpLink) { if (!idpLink) {
return linkingFailed(branding);
} else {
return linkingSuccess( return linkingSuccess(
foundUser.userId, foundUser.userId,
{ idpIntentId: id, idpIntentToken: token }, { idpIntentId: id, idpIntentToken: token },

View File

@@ -0,0 +1,26 @@
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { Alert, AlertType } from "../../alert";
import { DynamicTheme } from "../../dynamic-theme";
export async function linkingFailed(
branding?: BrandingSettings,
error?: string,
) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("linkingError.title")}</h1>
<p className="ztdl-p">{t("linkingError.description")}</p>
{error && (
<div className="w-full">
{<Alert type={AlertType.ALERT}>{error}</Alert>}
</div>
)}
</div>
</DynamicTheme>
);
}

View File

@@ -0,0 +1,29 @@
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { DynamicTheme } from "../../dynamic-theme";
import { IdpSignin } from "../../idp-signin";
export async function linkingSuccess(
userId: string,
idpIntent: { idpIntentId: string; idpIntentToken: string },
authRequestId?: string,
branding?: BrandingSettings,
) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("linkingSuccess.title")}</h1>
<p className="ztdl-p">{t("linkingSuccess.description")}</p>
<IdpSignin
userId={userId}
idpIntent={idpIntent}
authRequestId={authRequestId}
/>
</div>
</DynamicTheme>
);
}

View File

@@ -0,0 +1,23 @@
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { Alert, AlertType } from "../../alert";
import { DynamicTheme } from "../../dynamic-theme";
export async function loginFailed(branding?: BrandingSettings, error?: string) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("loginError.title")}</h1>
<p className="ztdl-p">{t("loginError.description")}</p>
{error && (
<div className="w-full">
{<Alert type={AlertType.ALERT}>{error}</Alert>}
</div>
)}
</div>
</DynamicTheme>
);
}

View File

@@ -0,0 +1,29 @@
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { DynamicTheme } from "../../dynamic-theme";
import { IdpSignin } from "../../idp-signin";
export async function loginSuccess(
userId: string,
idpIntent: { idpIntentId: string; idpIntentToken: string },
authRequestId?: string,
branding?: BrandingSettings,
) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("loginSuccess.title")}</h1>
<p className="ztdl-p">{t("loginSuccess.description")}</p>
<IdpSignin
userId={userId}
idpIntent={idpIntent}
authRequestId={authRequestId}
/>
</div>
</DynamicTheme>
);
}

View File

@@ -70,11 +70,7 @@ export function SignInWithIdp({
const renderIDPButton = (idp: IdentityProvider) => { const renderIDPButton = (idp: IdentityProvider) => {
const { id, name, type } = idp; const { id, name, type } = idp;
const onClick = () => startFlow(id, idpTypeToSlug(type)); const onClick = () => startFlow(id, idpTypeToSlug(type));
/* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */
// .filter((idp) =>
// linkOnly ? idp.config?.options.isLinkingAllowed : true,
// )
const components: Partial< const components: Partial<
Record< Record<
IdentityProviderType, IdentityProviderType,

View File

@@ -120,6 +120,7 @@ export async function sendVerificationRedirectWithoutCheck(command: {
if (!authMethodResponse || !authMethodResponse.authMethodTypes) { if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
return { error: "Could not load possible authenticators" }; return { error: "Could not load possible authenticators" };
} }
// if no authmethods are found on the user, redirect to set one up // if no authmethods are found on the user, redirect to set one up
if ( if (
authMethodResponse && authMethodResponse &&

View File

@@ -633,11 +633,15 @@ export async function verifyU2FRegistration(
return userService.verifyU2FRegistration(request, {}); return userService.verifyU2FRegistration(request, {});
} }
export async function getActiveIdentityProviders(orgId?: string) { export async function getActiveIdentityProviders(
return settingsService.getActiveIdentityProviders( orgId?: string,
{ ctx: makeReqCtx(orgId) }, linking_allowed?: boolean,
{}, ) {
); const props: any = { ctx: makeReqCtx(orgId) };
if (linking_allowed) {
props.linkingAllowed = linking_allowed;
}
return settingsService.getActiveIdentityProviders(props, {});
} }
/** /**