fix(login): fallback for idp login (#10876)

Closes #10671

# Which Problems Are Solved

Users with password authentication disabled in their organization were
seeing "Username Password not allowed!" error instead of being
redirected to their organization's configured Identity Provider. This
affected domain discovery and multi-tenancy use cases in Login V2.

# How the Problems Are Solved

- Updated `redirectUserToIDP` to accept optional `userId` and
`organization` parameters
- Added fallback logic to check organization-level IDPs via
`getActiveIdentityProviders`
- Updated all call sites to pass appropriate organization context
- Added test coverage for the fallback behavior

# Additional Changes

- Consolidated duplicate logic by removing
`redirectUserToSingleIDPIfAvailable` function, which is now handled by
the unified `redirectUserToIDP` function
- improved error handling on verification page

---------

Co-authored-by: Ramon <mail@conblem.me>
This commit is contained in:
Max Peintner
2025-10-21 11:04:33 +02:00
committed by GitHub
parent 2ad5cf141f
commit ff869482b1
13 changed files with 175 additions and 112 deletions

View File

@@ -386,7 +386,9 @@
"couldNotCreateSession": "Sitzung konnte nicht erstellt werden", "couldNotCreateSession": "Sitzung konnte nicht erstellt werden",
"noHostFound": "Kein Host gefunden", "noHostFound": "Kein Host gefunden",
"userAlreadyVerified": "Benutzer ist bereits verifiziert!", "userAlreadyVerified": "Benutzer ist bereits verifiziert!",
"couldNotResendInvite": "Einladung konnte nicht erneut gesendet werden" "couldNotResendInvite": "Einladung konnte nicht erneut gesendet werden",
"inviteSendFailed": "Einladungs-E-Mail konnte nicht gesendet werden",
"emailSendFailed": "Verifizierungs-E-Mail konnte nicht gesendet werden"
} }
}, },
"authenticator": { "authenticator": {

View File

@@ -386,7 +386,9 @@
"couldNotCreateSession": "Could not create session", "couldNotCreateSession": "Could not create session",
"noHostFound": "No host found", "noHostFound": "No host found",
"userAlreadyVerified": "User is already verified!", "userAlreadyVerified": "User is already verified!",
"couldNotResendInvite": "Could not resend invite" "couldNotResendInvite": "Could not resend invite",
"inviteSendFailed": "Failed to send invitation email",
"emailSendFailed": "Failed to send verification email"
} }
}, },
"authenticator": { "authenticator": {

View File

@@ -386,7 +386,9 @@
"couldNotCreateSession": "No se pudo crear la sesión", "couldNotCreateSession": "No se pudo crear la sesión",
"noHostFound": "No se encontró el host", "noHostFound": "No se encontró el host",
"userAlreadyVerified": "¡El usuario ya está verificado!", "userAlreadyVerified": "¡El usuario ya está verificado!",
"couldNotResendInvite": "No se pudo reenviar la invitación" "couldNotResendInvite": "No se pudo reenviar la invitación",
"inviteSendFailed": "No se pudo enviar el correo de invitación",
"emailSendFailed": "No se pudo enviar el correo de verificación"
} }
}, },
"authenticator": { "authenticator": {

View File

@@ -386,7 +386,9 @@
"couldNotCreateSession": "Impossibile creare la sessione", "couldNotCreateSession": "Impossibile creare la sessione",
"noHostFound": "Nessun host trovato", "noHostFound": "Nessun host trovato",
"userAlreadyVerified": "L'utente è già verificato!", "userAlreadyVerified": "L'utente è già verificato!",
"couldNotResendInvite": "Impossibile reinviare l'invito" "couldNotResendInvite": "Impossibile reinviare l'invito",
"inviteSendFailed": "Impossibile inviare l'email di invito",
"emailSendFailed": "Impossibile inviare l'email di verifica"
} }
}, },
"authenticator": { "authenticator": {

View File

@@ -287,6 +287,21 @@
"required": { "required": {
"code": "必須項目です" "code": "必須項目です"
} }
},
"errors": {
"couldNotResendEmail": "メールを再送信できませんでした",
"couldNotVerifyUser": "ユーザーを確認できませんでした",
"couldNotVerifyInvite": "招待を確認できませんでした",
"couldNotVerifyEmail": "メールアドレスを確認できませんでした",
"couldNotVerify": "確認できませんでした",
"couldNotLoadUser": "ユーザーを読み込めませんでした",
"couldNotLoadAuthenticators": "認証方法を読み込めませんでした",
"couldNotCreateSession": "セッションを作成できませんでした",
"noHostFound": "ホストが見つかりません",
"userAlreadyVerified": "ユーザーは既に確認済みです!",
"couldNotResendInvite": "招待を再送信できませんでした",
"inviteSendFailed": "招待メールの送信に失敗しました",
"emailSendFailed": "確認メールの送信に失敗しました"
} }
}, },
"authenticator": { "authenticator": {

View File

@@ -386,7 +386,9 @@
"couldNotCreateSession": "Nie udało się utworzyć sesji", "couldNotCreateSession": "Nie udało się utworzyć sesji",
"noHostFound": "Nie znaleziono hosta", "noHostFound": "Nie znaleziono hosta",
"userAlreadyVerified": "Użytkownik jest już zweryfikowany!", "userAlreadyVerified": "Użytkownik jest już zweryfikowany!",
"couldNotResendInvite": "Nie udało się ponownie wysłać zaproszenia" "couldNotResendInvite": "Nie udało się ponownie wysłać zaproszenia",
"inviteSendFailed": "Nie udało się wysłać wiadomości e-mail z zaproszeniem",
"emailSendFailed": "Nie udało się wysłać wiadomości e-mail weryfikacyjnej"
} }
}, },
"authenticator": { "authenticator": {

View File

@@ -386,7 +386,9 @@
"couldNotCreateSession": "Не удалось создать сеанс", "couldNotCreateSession": "Не удалось создать сеанс",
"noHostFound": "Хост не найден", "noHostFound": "Хост не найден",
"userAlreadyVerified": "Пользователь уже подтверждён!", "userAlreadyVerified": "Пользователь уже подтверждён!",
"couldNotResendInvite": "Не удалось повторно отправить приглашение" "couldNotResendInvite": "Не удалось повторно отправить приглашение",
"inviteSendFailed": "Не удалось отправить письмо с приглашением",
"emailSendFailed": "Не удалось отправить письмо с подтверждением"
} }
}, },
"authenticator": { "authenticator": {

View File

@@ -386,7 +386,9 @@
"couldNotCreateSession": "无法创建会话", "couldNotCreateSession": "无法创建会话",
"noHostFound": "未找到主机", "noHostFound": "未找到主机",
"userAlreadyVerified": "用户已验证!", "userAlreadyVerified": "用户已验证!",
"couldNotResendInvite": "无法重新发送邀请" "couldNotResendInvite": "无法重新发送邀请",
"inviteSendFailed": "发送邀请邮件失败",
"emailSendFailed": "发送验证邮件失败"
} }
}, },
"authenticator": { "authenticator": {

View File

@@ -193,6 +193,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
</div> </div>
<SignInWithIdp <SignInWithIdp
showLabel={false}
identityProviders={identityProviders} identityProviders={identityProviders}
requestId={requestId} requestId={requestId}
organization={sessionWithData.factors?.user?.organizationId} organization={sessionWithData.factors?.user?.organizationId}

View File

@@ -36,6 +36,8 @@ export default async function Page(props: { searchParams: Promise<any> }) {
let human: HumanUser | undefined; let human: HumanUser | undefined;
let id: string | undefined; let id: string | undefined;
let error: string | undefined;
const doSend = send === "true"; const doSend = send === "true";
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
@@ -49,9 +51,9 @@ export default async function Page(props: { searchParams: Promise<any> }) {
urlTemplate: urlTemplate:
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + `${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(requestId ? `&requestId=${requestId}` : ""), (requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => { }).catch((apiError) => {
console.error("Could not send invitation email", error); console.error("Could not send invitation email", apiError);
throw Error("Failed to send invitation email"); error = "inviteSendFailed";
}); });
} else { } else {
await sendEmailCode({ await sendEmailCode({
@@ -59,9 +61,9 @@ export default async function Page(props: { searchParams: Promise<any> }) {
urlTemplate: urlTemplate:
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + `${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(requestId ? `&requestId=${requestId}` : ""), (requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => { }).catch((apiError) => {
console.error("Could not send verification email", error); console.error("Could not send verification email", apiError);
throw Error("Failed to send verification email"); error = "emailSendFailed";
}); });
} }
} }
@@ -143,6 +145,14 @@ export default async function Page(props: { searchParams: Promise<any> }) {
</div> </div>
<div className="w-full"> <div className="w-full">
{error && (
<div className="py-4">
<Alert>
<Translated i18nKey={`errors.${error}`} namespace="verify" />
</Alert>
</div>
)}
{!id && ( {!id && (
<div className="py-4"> <div className="py-4">
<Alert> <Alert>

View File

@@ -1,7 +1,4 @@
import { import { LoginSettings, PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
LoginSettings,
PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { Alert, AlertType } from "./alert"; import { Alert, AlertType } from "./alert";
import { PASSKEYS, PASSWORD } from "./auth-methods"; import { PASSKEYS, PASSWORD } from "./auth-methods";
@@ -13,11 +10,7 @@ type Props = {
loginSettings: LoginSettings; loginSettings: LoginSettings;
}; };
export function ChooseAuthenticatorToSetup({ export function ChooseAuthenticatorToSetup({ authMethods, params, loginSettings }: Props) {
authMethods,
params,
loginSettings,
}: Props) {
if (authMethods.length !== 0) { if (authMethods.length !== 0) {
return ( return (
<Alert type={AlertType.ALERT}> <Alert type={AlertType.ALERT}>
@@ -26,26 +19,14 @@ export function ChooseAuthenticatorToSetup({
); );
} else { } else {
return ( return (
<> <div className="grid w-full grid-cols-1 gap-5 pt-4">
{loginSettings.passkeysType == PasskeysType.NOT_ALLOWED && {!authMethods.includes(AuthenticationMethodType.PASSWORD) &&
!loginSettings.allowUsernamePassword && ( loginSettings.allowUsernamePassword &&
<Alert type={AlertType.ALERT}> PASSWORD(false, "/password/set?" + params)}
<Translated {!authMethods.includes(AuthenticationMethodType.PASSKEY) &&
i18nKey="noMethodsAvailable" loginSettings.passkeysType == PasskeysType.ALLOWED &&
namespace="authenticator" PASSKEYS(false, "/passkey/set?" + params)}
/> </div>
</Alert>
)}
<div className="grid w-full grid-cols-1 gap-5 pt-4">
{!authMethods.includes(AuthenticationMethodType.PASSWORD) &&
loginSettings.allowUsernamePassword &&
PASSWORD(false, "/password/set?" + params)}
{!authMethods.includes(AuthenticationMethodType.PASSKEY) &&
loginSettings.passkeysType == PasskeysType.ALLOWED &&
PASSKEYS(false, "/passkey/set?" + params)}
</div>
</>
); );
} }
} }

View File

@@ -249,6 +249,7 @@ describe("sendLoginname", () => {
authMethodTypes: [AuthenticationMethodType.PASSWORD], authMethodTypes: [AuthenticationMethodType.PASSWORD],
}); });
mockListIDPLinks.mockResolvedValue({ result: [] }); mockListIDPLinks.mockResolvedValue({ result: [] });
mockGetActiveIdentityProviders.mockResolvedValue({ identityProviders: [] });
const result = await sendLoginname({ const result = await sendLoginname({
loginName: "user@example.com", loginName: "user@example.com",
@@ -259,6 +260,29 @@ describe("sendLoginname", () => {
}); });
}); });
test("should redirect to organization IDP when password not allowed, no user IDP links, but organization has active IDP", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
mockListIDPLinks.mockResolvedValue({ result: [] });
mockGetActiveIdentityProviders.mockResolvedValue({
identityProviders: [{ id: "org-idp-123", type: 0 }],
});
mockIdpTypeToSlug.mockReturnValue("google");
mockStartIdentityProviderFlow.mockResolvedValue("https://org-idp.example.com/auth");
const result = await sendLoginname({
loginName: "user@example.com",
});
expect(result).toEqual({ redirect: "https://org-idp.example.com/auth" });
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
orgId: "org123", // User's organization from resourceOwner
});
});
test("should redirect to passkey when user has only passkey method and it's allowed", async () => { test("should redirect to passkey when user has only passkey method and it's allowed", async () => {
mockGetLoginSettings.mockResolvedValue({ passkeysType: PasskeysType.ALLOWED }); mockGetLoginSettings.mockResolvedValue({ passkeysType: PasskeysType.ALLOWED });
mockListAuthenticationMethodTypes.mockResolvedValue({ mockListAuthenticationMethodTypes.mockResolvedValue({
@@ -373,6 +397,7 @@ describe("sendLoginname", () => {
authMethodTypes: [AuthenticationMethodType.PASSWORD], authMethodTypes: [AuthenticationMethodType.PASSWORD],
}); });
mockListIDPLinks.mockResolvedValue({ result: [] }); mockListIDPLinks.mockResolvedValue({ result: [] });
mockGetActiveIdentityProviders.mockResolvedValue({ identityProviders: [] });
const result = await sendLoginname({ const result = await sendLoginname({
loginName: "user@example.com", loginName: "user@example.com",

View File

@@ -23,6 +23,7 @@ import {
} from "../zitadel"; } from "../zitadel";
import { createSessionAndUpdateCookie } from "./cookie"; import { createSessionAndUpdateCookie } from "./cookie";
import { getOriginalHost } from "./host"; import { getOriginalHost } from "./host";
import { IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb";
export type SendLoginnameCommand = { export type SendLoginnameCommand = {
loginName: string; loginName: string;
@@ -68,63 +69,72 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const { result: potentialUsers } = searchResult; const { result: potentialUsers } = searchResult;
const redirectUserToSingleIDPIfAvailable = async () => { const redirectUserToIDP = async (userId?: string, organization?: string) => {
const identityProviders = await getActiveIdentityProviders({ // If userId is provided, check for user-specific IDP links first
serviceUrl, let identityProviders: IDPLink[] = [];
orgId: command.organization, if (userId) {
}).then((resp) => { identityProviders = await listIDPLinks({
return resp.identityProviders;
});
if (identityProviders.length === 1) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const identityProviderType = identityProviders[0].type;
const provider = idpTypeToSlug(identityProviderType);
const params = new URLSearchParams();
if (command.requestId) {
params.set("requestId", command.requestId);
}
if (command.organization) {
params.set("organization", command.organization);
}
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
serviceUrl, serviceUrl,
idpId: identityProviders[0].id, userId,
urls: { }).then((resp) => {
successUrl: return resp.result;
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/idp/${provider}/success?` + });
new URLSearchParams(params), }
failureUrl:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/idp/${provider}/failure?` + // If no IDP links exist for the user (or no userId provided), try to get active IDPs from the organization
new URLSearchParams(params), if (identityProviders.length === 0) {
}, const activeIdps = await getActiveIdentityProviders({
serviceUrl,
orgId: organization,
}).then((resp) => {
return resp.identityProviders;
}); });
if (!url) { // If exactly one active IDP exists in the organization, redirect to it
return { error: t("errors.couldNotStartIDPFlow") }; if (activeIdps.length === 1) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const identityProviderType = activeIdps[0].type;
const provider = idpTypeToSlug(identityProviderType);
const params = new URLSearchParams();
if (userId) {
params.set("userId", userId);
}
if (command.requestId) {
params.set("requestId", command.requestId);
}
if (organization) {
params.set("organization", organization);
}
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
serviceUrl,
idpId: activeIdps[0].id,
urls: {
successUrl:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/idp/${provider}/success?` +
new URLSearchParams(params),
failureUrl:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/idp/${provider}/failure?` +
new URLSearchParams(params),
},
});
if (!url) {
return { error: t("errors.couldNotStartIDPFlow") };
}
return { redirect: url };
} }
return { redirect: url };
} }
};
const redirectUserToIDP = async (userId: string) => {
const identityProviders = await listIDPLinks({
serviceUrl,
userId,
}).then((resp) => {
return resp.result;
});
if (identityProviders.length === 1) { if (identityProviders.length === 1) {
const _headers = await headers(); const _headers = await headers();
@@ -147,14 +157,18 @@ 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({ userId }); const params = new URLSearchParams();
if (userId) {
params.set("userId", userId);
}
if (command.requestId) { if (command.requestId) {
params.set("requestId", command.requestId); params.set("requestId", command.requestId);
} }
if (command.organization) { if (organization) {
params.set("organization", command.organization); params.set("organization", organization);
} }
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
@@ -240,6 +254,9 @@ export async function sendLoginname(command: SendLoginnameCommand) {
return { error: t("errors.initialUserNotSupported") }; return { error: t("errors.initialUserNotSupported") };
} }
// Resolve organization from command or session
const organization = command.organization ?? session.factors?.user?.organizationId;
const methods = await listAuthenticationMethodTypes({ const methods = await listAuthenticationMethodTypes({
serviceUrl, serviceUrl,
userId: session.factors?.user?.id, userId: session.factors?.user?.id,
@@ -250,15 +267,15 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const params = new URLSearchParams({ const params = new URLSearchParams({
loginName: session.factors?.user?.loginName as string, loginName: session.factors?.user?.loginName as string,
send: "true", // set this to true to request a new code immediately send: "true", // set this to true to request a new code immediately
invite: "true", invite: "true", // humanUser?.email?.isVerified ? "false" : "true", // sendInviteEmailCode results in an error if user is already initialized
}); });
if (command.requestId) { if (command.requestId) {
params.append("requestId", command.requestId); params.append("requestId", command.requestId);
} }
if (command.organization || session.factors?.user?.organizationId) { if (organization) {
params.append("organization", command.organization ?? (session.factors?.user?.organizationId as string)); params.append("organization", organization);
} }
return { redirect: `/verify?` + params }; return { redirect: `/verify?` + params };
@@ -270,7 +287,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
case AuthenticationMethodType.PASSWORD: // user has only password as auth method case AuthenticationMethodType.PASSWORD: // user has only password as auth method
if (!userLoginSettings?.allowUsernamePassword) { if (!userLoginSettings?.allowUsernamePassword) {
// Check if user has IDPs available as alternative, that could eventually be used to register/link. // Check if user has IDPs available as alternative, that could eventually be used to register/link.
const idpResp = await redirectUserToIDP(userId); const idpResp = await redirectUserToIDP(userId, organization);
if (idpResp?.redirect) { if (idpResp?.redirect) {
return idpResp; return idpResp;
} }
@@ -286,8 +303,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// TODO: does this have to be checked in loginSettings.allowDomainDiscovery // TODO: does this have to be checked in loginSettings.allowDomainDiscovery
if (command.organization || session.factors?.user?.organizationId) { if (organization) {
paramsPassword.append("organization", command.organization ?? session.factors?.user?.organizationId); paramsPassword.append("organization", organization);
} }
if (command.requestId) { if (command.requestId) {
@@ -312,14 +329,14 @@ export async function sendLoginname(command: SendLoginnameCommand) {
paramsPasskey.append("requestId", command.requestId); paramsPasskey.append("requestId", command.requestId);
} }
if (command.organization || session.factors?.user?.organizationId) { if (organization) {
paramsPasskey.append("organization", command.organization ?? session.factors?.user?.organizationId); paramsPasskey.append("organization", organization);
} }
return { redirect: "/passkey?" + paramsPasskey }; return { redirect: "/passkey?" + paramsPasskey };
case AuthenticationMethodType.IDP: case AuthenticationMethodType.IDP:
const resp = await redirectUserToIDP(userId); const resp = await redirectUserToIDP(userId, organization);
if (resp?.error) { if (resp?.error) {
return { error: resp.error }; return { error: resp.error };
@@ -339,13 +356,13 @@ export async function sendLoginname(command: SendLoginnameCommand) {
passkeyParams.append("requestId", command.requestId); passkeyParams.append("requestId", command.requestId);
} }
if (command.organization || session.factors?.user?.organizationId) { if (organization) {
passkeyParams.append("organization", command.organization ?? session.factors?.user?.organizationId); passkeyParams.append("organization", organization);
} }
return { redirect: "/passkey?" + passkeyParams }; return { redirect: "/passkey?" + passkeyParams };
} else if (methods.authMethodTypes.includes(AuthenticationMethodType.IDP)) { } else if (methods.authMethodTypes.includes(AuthenticationMethodType.IDP)) {
return redirectUserToIDP(userId); return redirectUserToIDP(userId, organization);
} else if (methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)) { } else if (methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)) {
// Check if password authentication is allowed // Check if password authentication is allowed
if (!userLoginSettings?.allowUsernamePassword) { if (!userLoginSettings?.allowUsernamePassword) {
@@ -363,8 +380,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
paramsPasswordDefault.append("requestId", command.requestId); paramsPasswordDefault.append("requestId", command.requestId);
} }
if (command.organization || session.factors?.user?.organizationId) { if (organization) {
paramsPasswordDefault.append("organization", command.organization ?? session.factors?.user?.organizationId); paramsPasswordDefault.append("organization", organization);
} }
return { return {
@@ -376,7 +393,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// user not found, check if register is enabled on instance / organization context // user not found, check if register is enabled on instance / organization context
if (loginSettingsByContext?.allowRegister && !loginSettingsByContext?.allowUsernamePassword) { if (loginSettingsByContext?.allowRegister && !loginSettingsByContext?.allowUsernamePassword) {
const resp = await redirectUserToSingleIDPIfAvailable(); const resp = await redirectUserToIDP(undefined, command.organization);
if (resp) { if (resp) {
return resp; return resp;
} }