From 669f089d5b79cf59c82af4bdf6451d1f8c874651 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 17 Jan 2025 13:39:42 +0100 Subject: [PATCH 01/10] choose auth method for login --- .../(login)/idp/[provider]/failure/page.tsx | 46 ++++++++++++++----- .../choose-authenticator-to-login.tsx | 44 ++++++++++++++++++ apps/login/src/lib/server/loginname.ts | 2 +- 3 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 apps/login/src/components/choose-authenticator-to-login.tsx 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 a3cc0ee883..4e6bb95f95 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -1,17 +1,13 @@ +import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login"; import { DynamicTheme } from "@/components/dynamic-theme"; -import { getBrandingSettings } from "@/lib/zitadel"; -import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { + getBrandingSettings, + getLoginSettings, + listAuthenticationMethodTypes, +} from "@/lib/zitadel"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { getLocale, getTranslations } from "next-intl/server"; -// This configuration shows the given name in the respective IDP button as fallback -const PROVIDER_NAME_MAPPING: { - [provider: string]: string; -} = { - [IdentityProviderType.GOOGLE]: "Google", - [IdentityProviderType.GITHUB]: "GitHub", - [IdentityProviderType.AZURE_AD]: "Microsoft", -}; - export default async function Page(props: { searchParams: Promise>; params: Promise<{ provider: string }>; @@ -20,15 +16,41 @@ export default async function Page(props: { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "idp" }); - const { organization } = searchParams; + const { organization, userId } = searchParams; const branding = await getBrandingSettings(organization); + const loginSettings = await getLoginSettings(organization); + + let authMethods: AuthenticationMethodType[] = []; + if (userId) { + const authMethodsResponse = await listAuthenticationMethodTypes(userId); + if (authMethodsResponse.authMethodTypes) { + authMethods = authMethodsResponse.authMethodTypes; + } + } + + const params = new URLSearchParams({}); + if (organization) { + params.set("organization", organization); + } + if (userId) { + params.set("userId", userId); + } + return (

{t("loginError.title")}

{t("loginError.description")}

+ + {userId && authMethods.length && ( + + )}
); diff --git a/apps/login/src/components/choose-authenticator-to-login.tsx b/apps/login/src/components/choose-authenticator-to-login.tsx new file mode 100644 index 0000000000..9c2c5f2a17 --- /dev/null +++ b/apps/login/src/components/choose-authenticator-to-login.tsx @@ -0,0 +1,44 @@ +import { + LoginSettings, + PasskeysType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { useTranslations } from "next-intl"; +import { Alert, AlertType } from "./alert"; +import { PASSKEYS, PASSWORD } from "./auth-methods"; + +type Props = { + authMethods: AuthenticationMethodType[]; + params: URLSearchParams; + loginSettings: LoginSettings | undefined; +}; + +export function ChooseAuthenticatorToLogin({ + authMethods, + params, + loginSettings, +}: Props) { + const t = useTranslations("authenticator"); + + if (authMethods.length !== 0) { + return {t("allSetup")}; + } else { + return ( + <> + {loginSettings?.passkeysType == PasskeysType.NOT_ALLOWED && + !loginSettings.allowUsernamePassword && ( + {t("noMethodsAvailable")} + )} + +
+ {!authMethods.includes(AuthenticationMethodType.PASSWORD) && + loginSettings?.allowUsernamePassword && + PASSWORD(false, "/password/set?" + params)} + {!authMethods.includes(AuthenticationMethodType.PASSKEY) && + loginSettings?.passkeysType == PasskeysType.ALLOWED && + PASSKEYS(false, "/passkey/set?" + params)} +
+ + ); + } +} diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 65acb80bf0..162c64f02e 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -128,7 +128,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { const identityProviderType = idpTypeToIdentityProviderType(idpType); const provider = idpTypeToSlug(identityProviderType); - const params = new URLSearchParams(); + const params = new URLSearchParams({ userId }); if (command.authRequestId) { params.set("authRequestId", command.authRequestId); From bfd57dbb195a6128280201aff2090d707529c715 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 17 Jan 2025 16:23:51 +0100 Subject: [PATCH 02/10] show avatar, edit selection --- .../(login)/idp/[provider]/failure/page.tsx | 37 ++++++++++++++--- .../choose-authenticator-to-login.tsx | 40 ++++++++----------- 2 files changed, 48 insertions(+), 29 deletions(-) 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 4e6bb95f95..ced396d44f 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -1,10 +1,14 @@ +import { Alert, AlertType } from "@/components/alert"; import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { UserAvatar } from "@/components/user-avatar"; import { getBrandingSettings, getLoginSettings, + getUserByID, listAuthenticationMethodTypes, } from "@/lib/zitadel"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { getLocale, getTranslations } from "next-intl/server"; @@ -23,7 +27,18 @@ export default async function Page(props: { const loginSettings = await getLoginSettings(organization); let authMethods: AuthenticationMethodType[] = []; + let user: User | undefined = undefined; + let human: HumanUser | undefined = undefined; + if (userId) { + const userResponse = await getUserByID(userId); + if (userResponse) { + user = userResponse.user; + if (user?.type.case === "human") { + human = user.type.value as HumanUser; + } + } + const authMethodsResponse = await listAuthenticationMethodTypes(userId); if (authMethodsResponse.authMethodTypes) { authMethods = authMethodsResponse.authMethodTypes; @@ -42,14 +57,24 @@ export default async function Page(props: {

{t("loginError.title")}

-

{t("loginError.description")}

+ {t("loginError.description")} {userId && authMethods.length && ( - + <> + {user && human && ( + + )} + + + )}
diff --git a/apps/login/src/components/choose-authenticator-to-login.tsx b/apps/login/src/components/choose-authenticator-to-login.tsx index 9c2c5f2a17..f7e3cdf8f8 100644 --- a/apps/login/src/components/choose-authenticator-to-login.tsx +++ b/apps/login/src/components/choose-authenticator-to-login.tsx @@ -4,7 +4,6 @@ import { } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { useTranslations } from "next-intl"; -import { Alert, AlertType } from "./alert"; import { PASSKEYS, PASSWORD } from "./auth-methods"; type Props = { @@ -18,27 +17,22 @@ export function ChooseAuthenticatorToLogin({ params, loginSettings, }: Props) { - const t = useTranslations("authenticator"); + const t = useTranslations("idp"); - if (authMethods.length !== 0) { - return {t("allSetup")}; - } else { - return ( - <> - {loginSettings?.passkeysType == PasskeysType.NOT_ALLOWED && - !loginSettings.allowUsernamePassword && ( - {t("noMethodsAvailable")} - )} - -
- {!authMethods.includes(AuthenticationMethodType.PASSWORD) && - loginSettings?.allowUsernamePassword && - PASSWORD(false, "/password/set?" + params)} - {!authMethods.includes(AuthenticationMethodType.PASSKEY) && - loginSettings?.passkeysType == PasskeysType.ALLOWED && - PASSKEYS(false, "/passkey/set?" + params)} -
- - ); - } + return ( + <> + {authMethods.includes(AuthenticationMethodType.PASSWORD) && + loginSettings?.allowUsernamePassword && ( +
Choose an alternative method to login
+ )} +
+ {authMethods.includes(AuthenticationMethodType.PASSWORD) && + loginSettings?.allowUsernamePassword && + PASSWORD(false, "/password?" + params)} + {authMethods.includes(AuthenticationMethodType.PASSKEY) && + loginSettings?.passkeysType == PasskeysType.ALLOWED && + PASSKEYS(false, "/passkey?" + params)} +
+ + ); } From 4d3cc55e4feab1317b5c7fc14db155dee12b5ccc Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 21 Jan 2025 11:03:55 +0100 Subject: [PATCH 03/10] set loginname as param --- .../(login)/idp/[provider]/failure/page.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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 ced396d44f..f1a62ef069 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -30,6 +30,14 @@ export default async function Page(props: { let user: User | undefined = undefined; let human: HumanUser | undefined = undefined; + const params = new URLSearchParams({}); + if (organization) { + params.set("organization", organization); + } + if (userId) { + params.set("userId", userId); + } + if (userId) { const userResponse = await getUserByID(userId); if (userResponse) { @@ -37,6 +45,10 @@ export default async function Page(props: { if (user?.type.case === "human") { human = user.type.value as HumanUser; } + + if (user?.preferredLoginName) { + params.set("loginName", user.preferredLoginName); + } } const authMethodsResponse = await listAuthenticationMethodTypes(userId); @@ -45,14 +57,6 @@ export default async function Page(props: { } } - const params = new URLSearchParams({}); - if (organization) { - params.set("organization", organization); - } - if (userId) { - params.set("userId", userId); - } - return (
From cc724889f0e27847bed3652f3a826317012fdf1c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 31 Jan 2025 21:07:05 +0100 Subject: [PATCH 04/10] headers endpoint --- apps/login/src/app/headers/route.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 apps/login/src/app/headers/route.ts diff --git a/apps/login/src/app/headers/route.ts b/apps/login/src/app/headers/route.ts new file mode 100644 index 0000000000..9bfcf43011 --- /dev/null +++ b/apps/login/src/app/headers/route.ts @@ -0,0 +1,17 @@ +import { NextRequest, NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; +export const revalidate = false; +export const fetchCache = "default-no-store"; + +export async function GET(request: NextRequest) { + const headers = request.headers; + + // Convert headers to a plain object + const headersObject: Record = {}; + headers.forEach((value, key) => { + headersObject[key] = value; + }); + + return NextResponse.json(headersObject); +} From d469e92859c321af92f118dc339cc24eb0286235 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 3 Feb 2025 08:50:05 +0100 Subject: [PATCH 05/10] log request url --- apps/login/src/app/login/route.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 2cea4b3b0b..8114807ac0 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -205,6 +205,8 @@ export async function GET(request: NextRequest) { const authRequestId = searchParams.get("authRequest"); const sessionId = searchParams.get("sessionId"); + console.log("requesturl", request.url); + const _headers = await headers(); const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); From e2c74bb9106720b8a596f25b97f20fa28eab561d Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 3 Feb 2025 09:10:12 +0100 Subject: [PATCH 06/10] log service config --- apps/login/src/app/(login)/loginname/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index 7f8cb92812..2f195f7082 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -28,6 +28,8 @@ export default async function Page(props: { const _headers = await headers(); const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + console.log("serviceUrl", serviceUrl, serviceRegion); + let defaultOrganization; if (!organization) { const org: Organization | null = await getDefaultOrg({ From c6836136333dec7c3f452d81c096331ff20e8e64 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 3 Feb 2025 09:12:02 +0100 Subject: [PATCH 07/10] get host right --- apps/login/src/lib/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts index e543f35467..aa212f8a1b 100644 --- a/apps/login/src/lib/service.ts +++ b/apps/login/src/lib/service.ts @@ -69,7 +69,7 @@ export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): { if (host) { const [hostname, port] = host.split(":"); if (hostname !== "localhost") { - instanceUrl = host; + instanceUrl = host.startsWith("https://") ? host : `https://${host}`; } } } From 152d24f07641dbec1d5f9bb7c61d33b0f72656e0 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 3 Feb 2025 09:34:09 +0100 Subject: [PATCH 08/10] doc --- apps/login/src/lib/service.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts index aa212f8a1b..fe62bc2c9c 100644 --- a/apps/login/src/lib/service.ts +++ b/apps/login/src/lib/service.ts @@ -50,11 +50,20 @@ export async function createServiceForHost( return createClientFor(service)(transport); } +/** + * Extracts the service url and region from the headers if used in a multitenant context (x-zitadel-forward-host, x-zitade-region header) + * or falls back to the ZITADEL_API_URL for a self hosting deployment + * or falls back to the host header for a self hosting deployment using custom domains + * @param headers + * @returns the service url and region from the headers + * @throws if the service url could not be determined + * + */ export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): { serviceUrl: string; serviceRegion: string; } { - let instanceUrl: string = process.env.ZITADEL_API_URL; + let instanceUrl; const forwardedHost = headers.get("x-zitadel-forward-host"); // use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself @@ -63,6 +72,8 @@ export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): { instanceUrl = instanceUrl.startsWith("https://") ? instanceUrl : `https://${instanceUrl}`; + } else if (process.env.ZITADEL_API_URL) { + instanceUrl = process.env.ZITADEL_API_URL; } else { const host = headers.get("host"); @@ -74,6 +85,10 @@ export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): { } } + if (!instanceUrl) { + throw new Error("Service URL could not be determined"); + } + return { serviceUrl: instanceUrl, serviceRegion: headers.get("x-zitadel-region") || "", From db23e182dc8eeacb4f2e5e9ffc9730ec8d6ee386 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 5 Feb 2025 10:15:11 +0100 Subject: [PATCH 09/10] cleanup api route --- apps/login/src/app/headers/route.ts | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 apps/login/src/app/headers/route.ts diff --git a/apps/login/src/app/headers/route.ts b/apps/login/src/app/headers/route.ts deleted file mode 100644 index 9bfcf43011..0000000000 --- a/apps/login/src/app/headers/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -export const dynamic = "force-dynamic"; -export const revalidate = false; -export const fetchCache = "default-no-store"; - -export async function GET(request: NextRequest) { - const headers = request.headers; - - // Convert headers to a plain object - const headersObject: Record = {}; - headers.forEach((value, key) => { - headersObject[key] = value; - }); - - return NextResponse.json(headersObject); -} From 27e29a60105e9f7e040f18f2fc4a574c8e8e983a Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 5 Feb 2025 11:33:49 +0100 Subject: [PATCH 10/10] rm logs --- apps/login/src/app/(login)/loginname/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index 2f195f7082..7f8cb92812 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -28,8 +28,6 @@ export default async function Page(props: { const _headers = await headers(); const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); - console.log("serviceUrl", serviceUrl, serviceRegion); - let defaultOrganization; if (!organization) { const org: Organization | null = await getDefaultOrg({