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 c1e4de1b26..14ad793014 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -1,19 +1,19 @@ +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 { getServiceUrlFromHeaders } from "@/lib/service"; -import { getBrandingSettings } from "@/lib/zitadel"; -import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +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"; import { headers } from "next/headers"; -// 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 }>; @@ -22,7 +22,7 @@ export default async function Page(props: { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "idp" }); - const { organization } = searchParams; + const { organization, userId } = searchParams; const _headers = await headers(); const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); @@ -33,11 +33,74 @@ export default async function Page(props: { organization, }); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); + + let authMethods: AuthenticationMethodType[] = []; + 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({ + serviceUrl, + serviceRegion, + userId, + }); + if (userResponse) { + user = userResponse.user; + if (user?.type.case === "human") { + human = user.type.value as HumanUser; + } + + if (user?.preferredLoginName) { + params.set("loginName", user.preferredLoginName); + } + } + + const authMethodsResponse = await listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId, + }); + if (authMethodsResponse.authMethodTypes) { + authMethods = authMethodsResponse.authMethodTypes; + } + } + return (

{t("loginError.title")}

-

{t("loginError.description")}

+ {t("loginError.description")} + + {userId && authMethods.length && ( + <> + {user && human && ( + + )} + + + + )}
); 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); 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..f7e3cdf8f8 --- /dev/null +++ b/apps/login/src/components/choose-authenticator-to-login.tsx @@ -0,0 +1,38 @@ +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 { PASSKEYS, PASSWORD } from "./auth-methods"; + +type Props = { + authMethods: AuthenticationMethodType[]; + params: URLSearchParams; + loginSettings: LoginSettings | undefined; +}; + +export function ChooseAuthenticatorToLogin({ + authMethods, + params, + loginSettings, +}: Props) { + const t = useTranslations("idp"); + + 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)} +
+ + ); +} diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 18070ab76c..7bcf30723d 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -159,7 +159,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); diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts index e543f35467..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,17 +72,23 @@ 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"); if (host) { const [hostname, port] = host.split(":"); if (hostname !== "localhost") { - instanceUrl = host; + instanceUrl = host.startsWith("https://") ? host : `https://${host}`; } } } + if (!instanceUrl) { + throw new Error("Service URL could not be determined"); + } + return { serviceUrl: instanceUrl, serviceRegion: headers.get("x-zitadel-region") || "",