mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:27:32 +00:00
Merge pull request #342 from zitadel/qa
fix(idp): show alternative auth methods on failed idp
This commit is contained in:
@@ -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 { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service";
|
||||||
import { getBrandingSettings } from "@/lib/zitadel";
|
import {
|
||||||
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
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 { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
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: {
|
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 }>;
|
||||||
@@ -22,7 +22,7 @@ export default async function Page(props: {
|
|||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "idp" });
|
const t = await getTranslations({ locale, namespace: "idp" });
|
||||||
|
|
||||||
const { organization } = searchParams;
|
const { organization, userId } = searchParams;
|
||||||
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers);
|
||||||
@@ -33,11 +33,74 @@ export default async function Page(props: {
|
|||||||
organization,
|
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 (
|
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">
|
||||||
<h1>{t("loginError.title")}</h1>
|
<h1>{t("loginError.title")}</h1>
|
||||||
<p className="ztdl-p">{t("loginError.description")}</p>
|
<Alert type={AlertType.ALERT}>{t("loginError.description")}</Alert>
|
||||||
|
|
||||||
|
{userId && authMethods.length && (
|
||||||
|
<>
|
||||||
|
{user && human && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={user.preferredLoginName}
|
||||||
|
displayName={human?.profile?.displayName}
|
||||||
|
showDropdown={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ChooseAuthenticatorToLogin
|
||||||
|
authMethods={authMethods}
|
||||||
|
loginSettings={loginSettings}
|
||||||
|
params={params}
|
||||||
|
></ChooseAuthenticatorToLogin>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
|
@@ -205,6 +205,8 @@ export async function GET(request: NextRequest) {
|
|||||||
const authRequestId = searchParams.get("authRequest");
|
const authRequestId = searchParams.get("authRequest");
|
||||||
const sessionId = searchParams.get("sessionId");
|
const sessionId = searchParams.get("sessionId");
|
||||||
|
|
||||||
|
console.log("requesturl", request.url);
|
||||||
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
38
apps/login/src/components/choose-authenticator-to-login.tsx
Normal file
38
apps/login/src/components/choose-authenticator-to-login.tsx
Normal file
@@ -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 && (
|
||||||
|
<div className="ztdl-p">Choose an alternative method to login </div>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
||||||
|
{authMethods.includes(AuthenticationMethodType.PASSWORD) &&
|
||||||
|
loginSettings?.allowUsernamePassword &&
|
||||||
|
PASSWORD(false, "/password?" + params)}
|
||||||
|
{authMethods.includes(AuthenticationMethodType.PASSKEY) &&
|
||||||
|
loginSettings?.passkeysType == PasskeysType.ALLOWED &&
|
||||||
|
PASSKEYS(false, "/passkey?" + params)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -159,7 +159,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
const identityProviderType = idpTypeToIdentityProviderType(idpType);
|
const identityProviderType = idpTypeToIdentityProviderType(idpType);
|
||||||
const provider = idpTypeToSlug(identityProviderType);
|
const provider = idpTypeToSlug(identityProviderType);
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams({ userId });
|
||||||
|
|
||||||
if (command.authRequestId) {
|
if (command.authRequestId) {
|
||||||
params.set("authRequestId", command.authRequestId);
|
params.set("authRequestId", command.authRequestId);
|
||||||
|
@@ -50,11 +50,20 @@ export async function createServiceForHost<T extends ServiceClass>(
|
|||||||
return createClientFor<T>(service)(transport);
|
return createClientFor<T>(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): {
|
export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): {
|
||||||
serviceUrl: string;
|
serviceUrl: string;
|
||||||
serviceRegion: string;
|
serviceRegion: string;
|
||||||
} {
|
} {
|
||||||
let instanceUrl: string = process.env.ZITADEL_API_URL;
|
let instanceUrl;
|
||||||
|
|
||||||
const forwardedHost = headers.get("x-zitadel-forward-host");
|
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
|
// 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 = instanceUrl.startsWith("https://")
|
||||||
? instanceUrl
|
? instanceUrl
|
||||||
: `https://${instanceUrl}`;
|
: `https://${instanceUrl}`;
|
||||||
|
} else if (process.env.ZITADEL_API_URL) {
|
||||||
|
instanceUrl = process.env.ZITADEL_API_URL;
|
||||||
} else {
|
} else {
|
||||||
const host = headers.get("host");
|
const host = headers.get("host");
|
||||||
|
|
||||||
if (host) {
|
if (host) {
|
||||||
const [hostname, port] = host.split(":");
|
const [hostname, port] = host.split(":");
|
||||||
if (hostname !== "localhost") {
|
if (hostname !== "localhost") {
|
||||||
instanceUrl = host;
|
instanceUrl = host.startsWith("https://") ? host : `https://${host}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!instanceUrl) {
|
||||||
|
throw new Error("Service URL could not be determined");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serviceUrl: instanceUrl,
|
serviceUrl: instanceUrl,
|
||||||
serviceRegion: headers.get("x-zitadel-region") || "",
|
serviceRegion: headers.get("x-zitadel-region") || "",
|
||||||
|
Reference in New Issue
Block a user