Skip to content

Commit

Permalink
Merge pull request #342 from zitadel/qa
Browse files Browse the repository at this point in the history
fix(idp): show alternative auth methods on failed idp
  • Loading branch information
peintnermax authored Feb 5, 2025
2 parents 0bc2c1d + 27e29a6 commit cc9cc30
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 16 deletions.
89 changes: 76 additions & 13 deletions apps/login/src/app/(login)/idp/[provider]/failure/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string | number | symbol, string | undefined>>;
params: Promise<{ provider: string }>;
Expand All @@ -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);
Expand All @@ -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 (
<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>
<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>
</DynamicTheme>
);
Expand Down
2 changes: 2 additions & 0 deletions apps/login/src/app/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
38 changes: 38 additions & 0 deletions apps/login/src/components/choose-authenticator-to-login.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
}
2 changes: 1 addition & 1 deletion apps/login/src/lib/server/loginname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 17 additions & 2 deletions apps/login/src/lib/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,20 @@ export async function createServiceForHost<T extends ServiceClass>(
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): {
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
Expand All @@ -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") || "",
Expand Down

0 comments on commit cc9cc30

Please sign in to comment.