diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index ba0fce27..5e8756c8 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -183,7 +183,8 @@ "title": "Authentifizierungsmethode auswählen", "description": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten.", "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": { "unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.", diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 26530438..3101f222 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -183,7 +183,8 @@ "title": "Choose authentication method", "description": "Select the method you would like to authenticate", "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": { "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.", diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 35bbb938..5a9b6f63 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -183,7 +183,8 @@ "title": "Seleccionar método de autenticación", "description": "Selecciona el método con el que deseas autenticarte", "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": { "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.", diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 4e21e3dc..1423c43c 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -183,7 +183,8 @@ "title": "Seleziona metodo di autenticazione", "description": "Seleziona il metodo con cui desideri autenticarti", "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": { "unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.", diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index 818a8b44..acd03cc5 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -183,7 +183,8 @@ "title": "选择认证方式", "description": "选择您想使用的认证方法", "noMethodsAvailable": "没有可用的认证方法", - "allSetup": "您已经设置好了一个认证器!" + "allSetup": "您已经设置好了一个认证器!", + "linkWithIDP": "或将其与身份提供者关联" }, "error": { "unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。", diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 36294c8c..7203484d 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -2,10 +2,12 @@ import { Alert } from "@/components/alert"; import { BackButton } from "@/components/back-button"; import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { loadMostRecentSession } from "@/lib/session"; import { + getActiveIdentityProviders, getBrandingSettings, getLoginSettings, getSession, @@ -86,13 +88,12 @@ export default async function Page(props: { sessionWithData.factors?.user?.organizationId, ); - /* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */ - - // const identityProviders = await getActiveIdentityProviders( - // sessionWithData.factors?.user?.organizationId, - // ).then((resp) => { - // return resp.identityProviders; - // }); + const identityProviders = await getActiveIdentityProviders( + sessionWithData.factors?.user?.organizationId, + true, + ).then((resp) => { + return resp.identityProviders; + }); const params = new URLSearchParams({ 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); } - const host = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000"; - return (
@@ -136,21 +133,18 @@ export default async function Page(props: { > )} - {/* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */} - - {/*

- or sign in with an Identity Provider -

+
+

{t("linkWithIDP")}

+
{loginSettings?.allowExternalIdp && identityProviders && ( - )} */} + )}
diff --git a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx index a8bc6d9c..a3cc0ee8 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -28,7 +28,7 @@ export default async function Page(props: {

{t("loginError.title")}

-
{t("loginError.description")}
+

{t("loginError.description")}

); 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 c81d43de..0de29fd1 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -1,6 +1,9 @@ -import { Alert, AlertType } from "@/components/alert"; 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 { addHuman, @@ -15,7 +18,6 @@ import { 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 { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { AddHumanUserRequest, AddHumanUserRequestSchema, @@ -24,75 +26,6 @@ import { getLocale, getTranslations } from "next-intl/server"; const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; -async function loginFailed(branding?: BrandingSettings, error: string = "") { - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "idp" }); - - return ( - -
-

{t("loginError.title")}

-

{t("loginError.description")}

- {error && ( -
- {{error}} -
- )} -
-
- ); -} - -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 ( - -
-

{t("loginSuccess.title")}

-

{t("loginSuccess.description")}

- - -
-
- ); -} - -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 ( - -
-

{t("linkingSuccess.title")}

-

{t("linkingSuccess.description")}

- - -
-
- ); -} - export default async function Page(props: { searchParams: Promise>; params: Promise<{ provider: string }>; @@ -139,6 +72,39 @@ export default async function Page(props: { 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 if (options?.isLinkingAllowed) { let foundUser; @@ -166,31 +132,24 @@ export default async function Page(props: { } if (foundUser) { - const idpLink = await addIDPLink( - { - id: idpInformation.idpId, - userId: idpInformation.userId, - userName: idpInformation.userName, - }, - foundUser.userId, - ).catch((error) => { - return ( - -
-

{t("linkingError.title")}

-
- { - - {t("linkingError.description")} - - } -
-
-
+ let idpLink; + try { + idpLink = await addIDPLink( + { + id: idpInformation.idpId, + userId: idpInformation.userId, + userName: idpInformation.userName, + }, + foundUser.userId, ); - }); + } catch (error) { + console.error(error); + return linkingFailed(branding); + } - if (idpLink) { + if (!idpLink) { + return linkingFailed(branding); + } else { return linkingSuccess( foundUser.userId, { idpIntentId: id, idpIntentToken: token }, diff --git a/apps/login/src/components/idps/pages/linking-failed.tsx b/apps/login/src/components/idps/pages/linking-failed.tsx new file mode 100644 index 00000000..f4016a33 --- /dev/null +++ b/apps/login/src/components/idps/pages/linking-failed.tsx @@ -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 ( + +
+

{t("linkingError.title")}

+

{t("linkingError.description")}

+ {error && ( +
+ {{error}} +
+ )} +
+
+ ); +} diff --git a/apps/login/src/components/idps/pages/linking-success.tsx b/apps/login/src/components/idps/pages/linking-success.tsx new file mode 100644 index 00000000..66098ed6 --- /dev/null +++ b/apps/login/src/components/idps/pages/linking-success.tsx @@ -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 ( + +
+

{t("linkingSuccess.title")}

+

{t("linkingSuccess.description")}

+ + +
+
+ ); +} diff --git a/apps/login/src/components/idps/pages/login-failed.tsx b/apps/login/src/components/idps/pages/login-failed.tsx new file mode 100644 index 00000000..e4187af7 --- /dev/null +++ b/apps/login/src/components/idps/pages/login-failed.tsx @@ -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 ( + +
+

{t("loginError.title")}

+

{t("loginError.description")}

+ {error && ( +
+ {{error}} +
+ )} +
+
+ ); +} diff --git a/apps/login/src/components/idps/pages/login-success.tsx b/apps/login/src/components/idps/pages/login-success.tsx new file mode 100644 index 00000000..3a9a3719 --- /dev/null +++ b/apps/login/src/components/idps/pages/login-success.tsx @@ -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 ( + +
+

{t("loginSuccess.title")}

+

{t("loginSuccess.description")}

+ + +
+
+ ); +} diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index fd675b62..972f501c 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -70,11 +70,7 @@ export function SignInWithIdp({ const renderIDPButton = (idp: IdentityProvider) => { const { id, name, type } = idp; 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< Record< IdentityProviderType, diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index ac4d6181..e2484b64 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -678,11 +678,15 @@ export async function verifyU2FRegistration( return userService.verifyU2FRegistration(request, {}); } -export async function getActiveIdentityProviders(orgId?: string) { - return settingsService.getActiveIdentityProviders( - { ctx: makeReqCtx(orgId) }, - {}, - ); +export async function getActiveIdentityProviders( + orgId?: string, + linking_allowed?: boolean, +) { + const props: any = { ctx: makeReqCtx(orgId) }; + if (linking_allowed) { + props.linkingAllowed = linking_allowed; + } + return settingsService.getActiveIdentityProviders(props, {}); } /**