fix(login, console): replace allowUsernamePassword with allowLocalAuthentication in login, change translation in console (#11413)

# Which Problems Are Solved

This PR deprecates and replaces the use of `allowUsernamePassword` with
`allowLocalAuthentication`.
It additionally disables the username on organizations that disallow
local authentication. In such a case, only IDPs are shown.

# How the Problems Are Solved

- replace `allowUsernamePassword` by `allowLocalAuthentication`
- disable username form if !allowLocalAuthentication
- translate property in console UI
This commit is contained in:
Max Peintner
2026-02-04 12:54:37 +00:00
committed by GitHub
parent 6f07b8deee
commit 38dbd4cf3e
46 changed files with 171 additions and 142 deletions
@@ -83,7 +83,7 @@ describe("verify invite", () => {
data: {
settings: {
passkeysType: 1,
allowUsernamePassword: true,
allowLocalAuthentication: true,
},
},
});
@@ -48,7 +48,7 @@ describe("login", () => {
data: {
settings: {
passkeysType: 1,
allowUsernamePassword: true,
allowLocalAuthentication: true,
},
},
});
@@ -15,7 +15,7 @@ describe("register", () => {
settings: {
passkeysType: 1,
allowRegister: true,
allowUsernamePassword: true,
allowLocalAuthentication: true,
defaultRedirectUri: "",
},
},
+1
View File
@@ -47,6 +47,7 @@
"userNotFound": "المستخدم غير موجود في النظام",
"couldNotCreateSession": "تعذر إنشاء جلسة للمستخدم",
"initialUserNotSupported": "المستخدم الأولي غير مدعوم",
"localAuthenticationNotAllowed": "المصادقة المحلية غير مسموح بها! اتصل بالمسؤول لمزيد من المعلومات.",
"usernamePasswordNotAllowed": "اسم المستخدم وكلمة المرور غير مسموح بهما! اتصل بالمسؤول لمزيد من المعلومات.",
"passkeysNotAllowed": "مفاتيح المرور غير مسموح بها! اتصل بالمسؤول لمزيد من المعلومات.",
"couldNotFindIdentityProvider": "تعذر العثور على مزود الهوية.",
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "Benutzer im System nicht gefunden",
"couldNotCreateSession": "Sitzung für Benutzer konnte nicht erstellt werden",
"initialUserNotSupported": "Initialer Benutzer wird nicht unterstützt",
"usernamePasswordNotAllowed": "Benutzername und Passwort nicht zulässig! Wenden Sie sich für weitere Informationen an Ihren Administrator.",
"localAuthenticationNotAllowed": "Lokale Authentifizierung nicht zulässig! Wenden Sie sich für weitere Informationen an Ihren Administrator.",
"passkeysNotAllowed": "Passkeys nicht zulässig! Wenden Sie sich für weitere Informationen an Ihren Administrator.",
"couldNotFindIdentityProvider": "Identitätsanbieter konnte nicht gefunden werden.",
"userNotActive": "Das Benutzerkonto ist inaktiv. Wenden Sie sich für weitere Informationen an Ihren Administrator."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "User not found in the system",
"couldNotCreateSession": "Could not create session for user",
"initialUserNotSupported": "Initial User not supported",
"usernamePasswordNotAllowed": "Username Password not allowed! Contact your administrator for more information.",
"localAuthenticationNotAllowed": "Local authentication is not allowed! Contact your administrator for more information.",
"passkeysNotAllowed": "Passkeys not allowed! Contact your administrator for more information.",
"couldNotFindIdentityProvider": "Could not find identity provider.",
"userNotActive": "User is not active. Contact your administrator for more information."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "Usuario no encontrado en el sistema",
"couldNotCreateSession": "No se pudo crear la sesión para el usuario",
"initialUserNotSupported": "Usuario inicial no admitido",
"usernamePasswordNotAllowed": "El inicio de sesión con usuario y contraseña no está permitido. Contacte con su administrador para más información.",
"localAuthenticationNotAllowed": "¡La autenticación local no está permitida! Contacte con su administrador para más información.",
"passkeysNotAllowed": "Las passkeys no están permitidas. Contacte con su administrador para más información.",
"couldNotFindIdentityProvider": "No se pudo encontrar el proveedor de identidad.",
"userNotActive": "El usuario no está activo. Contacte con su administrador para más información."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "Utilisateur non trouvé dans le système",
"couldNotCreateSession": "Impossible de créer une session pour l'utilisateur",
"initialUserNotSupported": "Utilisateur initial non pris en charge",
"usernamePasswordNotAllowed": "L'identifiant et le mot de passe ne sont pas autorisés ! Contactez votre administrateur pour plus d'informations.",
"localAuthenticationNotAllowed": "L'authentification locale n'est pas autorisée ! Contactez votre administrateur pour plus d'informations.",
"passkeysNotAllowed": "Les clés d'accès ne sont pas autorisées ! Contactez votre administrateur pour plus d'informations.",
"couldNotFindIdentityProvider": "Impossible de trouver le fournisseur d'identité.",
"userNotActive": "L'utilisateur n'est pas actif. Contactez votre administrateur pour plus d'informations."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "Utente non trovato nel sistema",
"couldNotCreateSession": "Impossibile creare la sessione per l'utente",
"initialUserNotSupported": "Utente iniziale non supportato",
"usernamePasswordNotAllowed": "Nome utente e password non consentiti! Contatta l'amministratore per maggiori informazioni.",
"localAuthenticationNotAllowed": "L'autenticazione locale non è consentita! Contatta il tuo amministratore per ulteriori informazioni.",
"passkeysNotAllowed": "Passkey non consentite! Contatta l'amministratore per maggiori informazioni.",
"couldNotFindIdentityProvider": "Impossibile trovare il provider di identità.",
"userNotActive": "L'utente non è attivo. Contatta l'amministratore per maggiori informazioni."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "ユーザーがシステムに見つかりません",
"couldNotCreateSession": "ユーザーのセッションを作成できませんでした",
"initialUserNotSupported": "初期ユーザーはサポートされていません",
"usernamePasswordNotAllowed": "ユーザー名とパスワードは許可されていません!詳細については管理者にお問い合わせください。",
"localAuthenticationNotAllowed": "ローカル認証は許可されていません! 詳細については管理者に問い合わせてください。",
"passkeysNotAllowed": "パスキーは許可されていません!詳細については管理者にお問い合わせください。",
"couldNotFindIdentityProvider": "IDプロバイダーが見つかりませんでした。",
"userNotActive": "ユーザーはアクティブではありません。詳細については管理者にお問い合わせください。"
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "Gebruiker niet in het systeem gevonden",
"couldNotCreateSession": "Kon sessie voor gebruiker niet aanmaken",
"initialUserNotSupported": "Initiële gebruiker wordt niet ondersteund",
"usernamePasswordNotAllowed": "Gebruikersnaam en wachtwoord niet toegestaan! Neem contact op met jouw beheerder voor meer informatie.",
"localAuthenticationNotAllowed": "Lokale authenticatie is niet toegestaan! Neem contact op met uw beheerder voor meer informatie.",
"passkeysNotAllowed": "Passkeys niet toegestaan! Neem contact op met jouw beheerder voor meer informatie.",
"couldNotFindIdentityProvider": "Kon identiteitsprovider niet vinden.",
"userNotActive": "Gebruiker is niet actief. Neem contact op met jouw beheerder voor meer informatie."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "Użytkownik nie został znaleziony w systemie",
"couldNotCreateSession": "Nie udało się utworzyć sesji dla użytkownika",
"initialUserNotSupported": "Użytkownik inicjalny nie jest obsługiwany",
"usernamePasswordNotAllowed": "Logowanie przy użyciu nazwy użytkownika i hasła jest niedozwolone! Skontaktuj się z administratorem, aby uzyskać więcej informacji.",
"localAuthenticationNotAllowed": "Uwierzytelnianie lokalne nie jest dozwolone! Skontaktuj się z administratorem, aby uzyskać więcej informacji.",
"passkeysNotAllowed": "Klucze dostępu są niedozwolone! Skontaktuj się z administratorem, aby uzyskać więcej informacji.",
"couldNotFindIdentityProvider": "Nie udało się znaleźć dostawcy tożsamości.",
"userNotActive": "Użytkownik jest nieaktywny. Skontaktuj się z administratorem, aby uzyskać więcej informacji."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "Пользователь не найден в системе",
"couldNotCreateSession": "Не удалось создать сессию для пользователя",
"initialUserNotSupported": "Начальный пользователь не поддерживается",
"usernamePasswordNotAllowed": "Использование имени пользователя и пароля не разрешено! Обратитесь к администратору за дополнительной информацией.",
"localAuthenticationNotAllowed": "Локальная аутентификация запрещена! Обратитесь к администратору за дополнительной информацией.",
"passkeysNotAllowed": "Ключи доступа не разрешены! Обратитесь к администратору за дополнительной информацией.",
"couldNotFindIdentityProvider": "Не удалось найти поставщика удостоверений.",
"userNotActive": "Пользователь неактивен. Обратитесь к администратору за дополнительной информацией."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "Kullanıcı sistemde bulunamadı",
"couldNotCreateSession": "Kullanıcı için oturum oluşturulamadı",
"initialUserNotSupported": "İlk kullanıcı desteklenmiyor",
"usernamePasswordNotAllowed": "Kullanıcı adı ve şifre izin verilmiyor! Daha fazla bilgi için yöneticinizle iletişime geçin.",
"localAuthenticationNotAllowed": "Yerel kimlik doğrulamasına izin verilmiyor! Daha fazla bilgi için yöneticinize başvurun.",
"passkeysNotAllowed": "Passkey'ler izin verilmiyor! Daha fazla bilgi için yöneticinizle iletişime geçin.",
"couldNotFindIdentityProvider": "Kimlik sağlayıcı bulunamadı.",
"userNotActive": "Kullanıcı aktif değil. Daha fazla bilgi için yöneticinizle iletişime geçin."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "Користувача не знайдено в системі",
"couldNotCreateSession": "Не вдалося створити сеанс для користувача",
"initialUserNotSupported": "Початковий користувач не підтримується",
"usernamePasswordNotAllowed": "Ім'я користувача та пароль не дозволені! Зверніться до адміністратора для отримання додаткової інформації.",
"localAuthenticationNotAllowed": "Локальна аутентифікація заборонена! Зверніться до адміністратора за додатковою інформацією.",
"passkeysNotAllowed": "Ключі доступу не дозволені! Зверніться до адміністратора для отримання додаткової інформації.",
"couldNotFindIdentityProvider": "Не вдалося знайти постачальника ідентичності.",
"userNotActive": "Користувач неактивний. Зверніться до адміністратора для отримання додаткової інформації."
+1 -1
View File
@@ -47,7 +47,7 @@
"userNotFound": "系统中未找到用户",
"couldNotCreateSession": "无法为用户创建会话",
"initialUserNotSupported": "不支持初始用户",
"usernamePasswordNotAllowed": "不允许使用用户名和密码!请联系管理员了解更多信息。",
"localAuthenticationNotAllowed": "不允许本地身份验证!请联系您的管理员以获取更多信息。",
"passkeysNotAllowed": "不允许使用 Passkey!请联系管理员了解更多信息。",
"couldNotFindIdentityProvider": "找不到身份提供者。",
"userNotActive": "用户未激活。请联系管理员了解更多信息。"
@@ -57,7 +57,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
</div>
<div className="w-full">
<UsernameForm
{loginSettings?.allowLocalAuthentication && <UsernameForm
loginName={loginName}
requestId={requestId}
organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user
@@ -66,7 +66,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
suffix={suffix}
submit={submit}
allowRegister={!!loginSettings?.allowRegister}
></UsernameForm>
></UsernameForm>}
{loginSettings?.allowExternalIdp && !!identityProviders?.length && (
<div className="w-full pb-4 pt-6">
@@ -75,6 +75,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
requestId={requestId}
organization={organization}
postErrorRedirectUrl="/loginname"
showLabel={loginSettings?.allowLocalAuthentication}
></SignInWithIdp>
</div>
)}
+1 -2
View File
@@ -13,7 +13,6 @@ import {
getPasswordComplexitySettings,
} from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { headers } from "next/headers";
@@ -93,7 +92,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
{legal &&
passwordComplexitySettings &&
organization &&
(loginSettings.allowUsernamePassword || loginSettings.passkeysType == PasskeysType.ALLOWED) && (
loginSettings.allowLocalAuthentication && (
<RegisterForm
idpCount={!loginSettings?.allowExternalIdp ? 0 : identityProviders.length}
legal={legal}
@@ -52,7 +52,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
</div>
<div className="w-full"></div>
</DynamicTheme>
) : loginSettings?.allowRegister && loginSettings.allowUsernamePassword ? (
) : loginSettings?.allowRegister && loginSettings.allowLocalAuthentication ? (
<DynamicTheme branding={branding}>
<div className="flex flex-col space-y-4">
<h1>
@@ -20,16 +20,17 @@ export function ChooseAuthenticatorToLogin({
return (
<>
{authMethods.includes(AuthenticationMethodType.PASSWORD) &&
loginSettings?.allowUsernamePassword && (
loginSettings?.allowLocalAuthentication && (
<div className="ztdl-p">
<Translated i18nKey="chooseAlternativeMethod" namespace="idp" />
</div>
)}
<div className="grid w-full grid-cols-1 gap-5 pt-4">
{authMethods.includes(AuthenticationMethodType.PASSWORD) &&
loginSettings?.allowUsernamePassword &&
loginSettings?.allowLocalAuthentication &&
PASSWORD(false, "/password?" + params)}
{authMethods.includes(AuthenticationMethodType.PASSKEY) &&
loginSettings?.allowLocalAuthentication &&
loginSettings?.passkeysType == PasskeysType.ALLOWED &&
PASSKEYS(false, "/passkey?" + params)}
</div>
@@ -21,7 +21,7 @@ export function ChooseAuthenticatorToSetup({ authMethods, params, loginSettings
return (
<div className="grid w-full grid-cols-1 gap-5 pt-4">
{!authMethods.includes(AuthenticationMethodType.PASSWORD) &&
loginSettings.allowUsernamePassword &&
loginSettings.allowLocalAuthentication &&
PASSWORD(false, "/password/set?" + params)}
{!authMethods.includes(AuthenticationMethodType.PASSKEY) &&
loginSettings.passkeysType == PasskeysType.ALLOWED &&
+4 -4
View File
@@ -159,7 +159,7 @@ export function RegisterForm({
<PrivacyPolicyCheckboxes legal={legal} onChange={setTosAndPolicyAccepted} />
)}
{/* show chooser if both methods are allowed */}
{loginSettings && loginSettings.allowUsernamePassword && loginSettings.passkeysType == PasskeysType.ALLOWED && (
{loginSettings && loginSettings.allowLocalAuthentication && loginSettings.passkeysType == PasskeysType.ALLOWED && (
<>
<p className="ztdl-p mb-6 mt-4 block text-left">
<Translated i18nKey="selectMethod" namespace="register" />
@@ -170,7 +170,7 @@ export function RegisterForm({
</div>
</>
)}
{!loginSettings?.allowUsernamePassword &&
{!loginSettings?.allowLocalAuthentication &&
loginSettings?.passkeysType !== PasskeysType.ALLOWED &&
(!loginSettings?.allowExternalIdp || !idpCount) && (
<div className="py-4">
@@ -194,9 +194,9 @@ export function RegisterForm({
disabled={loading || !canSubmit}
onClick={handleSubmit((values) => {
const usePasswordToContinue: boolean =
loginSettings?.allowUsernamePassword && loginSettings?.passkeysType == PasskeysType.ALLOWED
loginSettings?.allowLocalAuthentication && loginSettings?.passkeysType == PasskeysType.ALLOWED
? !(selected === methods[0]) // choose selection if both available
: !!loginSettings?.allowUsernamePassword; // if password is chosen
: !!loginSettings?.allowLocalAuthentication; // if password is chosen
// set password as default if only password is allowed
return submitAndContinue(values, usePasswordToContinue);
})}
+61 -41
View File
@@ -134,7 +134,7 @@ describe("sendLoginname", () => {
});
test("should return error when user search fails", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: true });
mockSearchUsers.mockResolvedValue({ error: "Search failed" });
const result = await sendLoginname({
@@ -145,7 +145,7 @@ describe("sendLoginname", () => {
});
test("should return error when search result has no result field", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: true });
mockSearchUsers.mockResolvedValue({});
const result = await sendLoginname({
@@ -156,7 +156,7 @@ describe("sendLoginname", () => {
});
test("should return error when more than one user found", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: true });
mockSearchUsers.mockResolvedValue({
result: [
{ userId: "user1", preferredLoginName: "[email protected]" },
@@ -192,7 +192,7 @@ describe("sendLoginname", () => {
};
beforeEach(() => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: true });
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
mockCreate.mockReturnValue({});
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
@@ -232,7 +232,7 @@ describe("sendLoginname", () => {
});
test("should attempt IDP redirect when password is not allowed but user has IDP links", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
@@ -253,7 +253,7 @@ describe("sendLoginname", () => {
});
test("should return error when password not allowed and no IDP links available", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
@@ -265,12 +265,12 @@ describe("sendLoginname", () => {
});
expect(result).toEqual({
error: "errors.usernamePasswordNotAllowed",
error: "errors.localAuthenticationNotAllowed",
});
});
test("should redirect to organization IDP when password not allowed, no user IDP links, but organization has active IDP", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
@@ -293,7 +293,7 @@ describe("sendLoginname", () => {
});
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, allowLocalAuthentication: true });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSKEY],
});
@@ -310,7 +310,25 @@ describe("sendLoginname", () => {
});
test("should return error when passkeys are not allowed", async () => {
mockGetLoginSettings.mockResolvedValue({ passkeysType: PasskeysType.NOT_ALLOWED });
mockGetLoginSettings.mockResolvedValue({ passkeysType: PasskeysType.NOT_ALLOWED, allowLocalAuthentication: true });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSKEY],
});
const result = await sendLoginname({
loginName: "[email protected]",
});
expect(result).toEqual({
error: "errors.passkeysNotAllowed",
});
});
test("should return error when passkeys are allowed but allowLocalAuthentication is false", async () => {
mockGetLoginSettings.mockResolvedValue({
passkeysType: PasskeysType.ALLOWED,
allowLocalAuthentication: false,
});
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSKEY],
});
@@ -342,7 +360,7 @@ describe("sendLoginname", () => {
test("should NOT create session when ignoreUnknownUsernames is true", async () => {
mockGetLoginSettings.mockResolvedValue({
allowUsernamePassword: true,
allowLocalAuthentication: true,
ignoreUnknownUsernames: true,
});
mockListAuthenticationMethodTypes.mockResolvedValue({
@@ -377,8 +395,8 @@ describe("sendLoginname", () => {
expect((result as any).redirect).toContain("altPassword=true"); // password is allowed
});
test("should not show password alternative when password is not allowed", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
test("should return error when allowLocalAuthentication is false (disabling both password and passkey)", async () => {
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.PASSKEY],
});
@@ -387,9 +405,9 @@ describe("sendLoginname", () => {
loginName: "[email protected]",
});
expect(result).toBeDefined();
expect(result?.redirect).toMatch(/^\/passkey\?/);
expect(result?.redirect).toContain("altPassword=false"); // password is not allowed
expect(result).toEqual({
error: "errors.localAuthenticationNotAllowed",
});
});
test("should redirect to IDP when no passkey but IDP available", async () => {
@@ -422,7 +440,7 @@ describe("sendLoginname", () => {
});
test("should return error when password is only method in multi-method scenario but not allowed", async () => {
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: false });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: false });
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
@@ -434,7 +452,7 @@ describe("sendLoginname", () => {
});
expect(result).toEqual({
error: "errors.usernamePasswordNotAllowed",
error: "errors.localAuthenticationNotAllowed",
});
});
});
@@ -448,7 +466,7 @@ describe("sendLoginname", () => {
test("should redirect to single IDP when register allowed but password not allowed", async () => {
mockGetLoginSettings.mockResolvedValue({
allowRegister: true,
allowUsernamePassword: false,
allowLocalAuthentication: false,
});
mockGetActiveIdentityProviders.mockResolvedValue({
identityProviders: [{ id: "idp123", type: "OIDC" }],
@@ -465,7 +483,7 @@ describe("sendLoginname", () => {
test("should redirect to register when both register and password allowed", async () => {
mockGetLoginSettings.mockResolvedValue({
allowRegister: true,
allowUsernamePassword: true,
allowLocalAuthentication: true,
ignoreUnknownUsernames: false,
});
@@ -504,7 +522,7 @@ describe("sendLoginname", () => {
test("should return error when user not found and no registration allowed", async () => {
mockGetLoginSettings.mockResolvedValue({
allowRegister: false,
allowUsernamePassword: true,
allowLocalAuthentication: true,
});
const result = await sendLoginname({
@@ -519,14 +537,14 @@ describe("sendLoginname", () => {
mockGetLoginSettings
.mockResolvedValueOnce({
allowRegister: true,
allowUsernamePassword: true,
allowLocalAuthentication: true,
ignoreUnknownUsernames: false,
})
// Mock login settings for discovered org - must include all necessary flags
.mockResolvedValueOnce({
allowDomainDiscovery: true,
allowRegister: true,
allowUsernamePassword: true,
allowLocalAuthentication: true,
ignoreUnknownUsernames: false,
});
@@ -559,13 +577,13 @@ describe("sendLoginname", () => {
mockGetLoginSettings
.mockResolvedValueOnce({
allowRegister: true,
allowUsernamePassword: false,
allowLocalAuthentication: false,
})
// Mock login settings for discovered org - must include all necessary flags
.mockResolvedValueOnce({
allowDomainDiscovery: true,
allowRegister: true,
allowUsernamePassword: false,
allowLocalAuthentication: false,
});
// Mock org discovery to return one org with matching domain
@@ -603,7 +621,7 @@ describe("sendLoginname", () => {
mockGetLoginSettings
.mockResolvedValueOnce({
allowRegister: true,
allowUsernamePassword: true,
allowLocalAuthentication: true,
ignoreUnknownUsernames: false,
})
// Mock login settings for org with domain discovery disabled
@@ -627,7 +645,7 @@ describe("sendLoginname", () => {
test("should not discover org if multiple orgs match the domain", async () => {
mockGetLoginSettings.mockResolvedValue({
allowRegister: true,
allowUsernamePassword: true,
allowLocalAuthentication: true,
ignoreUnknownUsernames: false,
});
@@ -651,7 +669,7 @@ describe("sendLoginname", () => {
test("should use provided organization instead of discovering when org context exists", async () => {
mockGetLoginSettings.mockResolvedValue({
allowRegister: true,
allowUsernamePassword: true,
allowLocalAuthentication: true,
ignoreUnknownUsernames: false,
});
@@ -669,11 +687,11 @@ describe("sendLoginname", () => {
expect(mockGetOrgsByDomain).not.toHaveBeenCalled();
});
test("should redirect to password when ignoreUnknownUsernames is true, allowRegister is true, but allowUsernamePassword is false (User not found)", async () => {
test("should redirect to password when ignoreUnknownUsernames is true, allowRegister is true, but allowLocalAuthentication is false (User not found)", async () => {
mockSearchUsers.mockResolvedValue({ result: [] });
mockGetLoginSettings.mockResolvedValue({
allowRegister: true,
allowUsernamePassword: false,
allowLocalAuthentication: false,
ignoreUnknownUsernames: true,
});
mockGetActiveIdentityProviders.mockResolvedValue({ identityProviders: [] });
@@ -747,7 +765,7 @@ describe("sendLoginname", () => {
});
mockGetLoginSettings.mockResolvedValue({
ignoreUnknownUsernames: true,
allowUsernamePassword: true,
allowLocalAuthentication: true,
});
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
@@ -777,7 +795,7 @@ describe("sendLoginname", () => {
});
mockGetLoginSettings.mockResolvedValue({
ignoreUnknownUsernames: true,
allowUsernamePassword: true,
allowLocalAuthentication: true,
});
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
@@ -809,7 +827,7 @@ describe("sendLoginname", () => {
};
mockGetLoginSettings.mockResolvedValue({
allowUsernamePassword: false,
allowLocalAuthentication: false,
ignoreUnknownUsernames: true,
});
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
@@ -826,7 +844,7 @@ describe("sendLoginname", () => {
ignoreUnknownUsernames: true,
});
expect(result).not.toEqual({ error: "errors.usernamePasswordNotAllowed" });
expect(result).not.toEqual({ error: "errors.localAuthenticationNotAllowed" });
expect(result).toHaveProperty("redirect");
expect((result as any).redirect).toMatch(/^\/password\?/);
});
@@ -892,7 +910,7 @@ describe("sendLoginname", () => {
},
};
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: true });
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
mockCreate.mockReturnValue({});
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
@@ -923,7 +941,7 @@ describe("sendLoginname", () => {
},
};
mockGetLoginSettings.mockResolvedValue({ allowUsernamePassword: true });
mockGetLoginSettings.mockResolvedValue({ allowLocalAuthentication: true });
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
mockCreate.mockReturnValue({});
mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} });
@@ -962,7 +980,7 @@ describe("sendLoginname", () => {
};
mockGetLoginSettings.mockResolvedValue({
allowUsernamePassword: true,
allowLocalAuthentication: true,
ignoreUnknownUsernames: true,
});
// Mock search result returns a user with resolved/different loginName
@@ -1006,6 +1024,7 @@ describe("sendLoginname", () => {
mockGetLoginSettings.mockResolvedValue({
passkeysType: PasskeysType.ALLOWED,
allowLocalAuthentication: true,
ignoreUnknownUsernames: true,
});
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
@@ -1047,6 +1066,7 @@ describe("sendLoginname", () => {
mockGetLoginSettings.mockResolvedValue({
passkeysType: PasskeysType.ALLOWED,
allowLocalAuthentication: true,
ignoreUnknownUsernames: true,
});
mockSearchUsers.mockResolvedValue({ result: [mockUser] });
@@ -1088,10 +1108,10 @@ describe("sendLoginname", () => {
// Mock implementation to return different settings based on organization
mockGetLoginSettings.mockImplementation(async (args: any) => {
if (args.organization === "context-org") {
return { allowUsernamePassword: true, ignoreUnknownUsernames: true };
return { allowLocalAuthentication: true, ignoreUnknownUsernames: true };
}
if (args.organization === "user-org") {
return { allowUsernamePassword: true, ignoreUnknownUsernames: false };
return { allowLocalAuthentication: true, ignoreUnknownUsernames: false };
}
return {};
});
@@ -1136,10 +1156,10 @@ describe("sendLoginname", () => {
mockGetLoginSettings.mockImplementation(async (args: any) => {
if (args.organization === "context-org") {
return { allowUsernamePassword: true, ignoreUnknownUsernames: false };
return { allowLocalAuthentication: true, ignoreUnknownUsernames: false };
}
if (args.organization === "user-org") {
return { allowUsernamePassword: true, ignoreUnknownUsernames: true };
return { allowLocalAuthentication: true, ignoreUnknownUsernames: true };
}
return {};
});
+16 -9
View File
@@ -336,7 +336,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const method = methods.authMethodTypes[0];
switch (method) {
case AuthenticationMethodType.PASSWORD: // user has only password as auth method
if (!userLoginSettings?.allowUsernamePassword) {
if (!userLoginSettings?.allowLocalAuthentication) {
// Check if user has IDPs available as alternative, that could eventually be used to register/link.
const idpResp = await redirectUserToIDP(userId, organization);
if (idpResp?.redirect) {
@@ -348,7 +348,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
}
return {
error: t("errors.usernamePasswordNotAllowed"),
error: t("errors.localAuthenticationNotAllowed"),
};
}
@@ -371,7 +371,10 @@ export async function sendLoginname(command: SendLoginnameCommand) {
};
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
if (userLoginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) {
if (
userLoginSettings?.passkeysType === PasskeysType.NOT_ALLOWED ||
!userLoginSettings?.allowLocalAuthentication
) {
if (command.ignoreUnknownUsernames) {
return preventUserEnumeration(command.organization);
}
@@ -406,12 +409,16 @@ export async function sendLoginname(command: SendLoginnameCommand) {
}
} else {
// prefer passkey in favor of other methods
if (methods.authMethodTypes.includes(AuthenticationMethodType.PASSKEY)) {
if (
methods.authMethodTypes.includes(AuthenticationMethodType.PASSKEY) &&
userLoginSettings?.passkeysType !== PasskeysType.NOT_ALLOWED &&
userLoginSettings?.allowLocalAuthentication
) {
const passkeyParams = new URLSearchParams({
loginName: command.ignoreUnknownUsernames
? command.loginName
: (session?.factors?.user?.loginName ?? user.preferredLoginName),
altPassword: `${methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD) && userLoginSettings?.allowUsernamePassword}`, // show alternative password option only if allowed
altPassword: `${methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD) && userLoginSettings?.allowLocalAuthentication}`, // show alternative password option only if allowed
});
if (command.requestId) {
@@ -427,12 +434,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
return redirectUserToIDP(userId, organization);
} else if (methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)) {
// Check if password authentication is allowed
if (!userLoginSettings?.allowUsernamePassword) {
if (!userLoginSettings?.allowLocalAuthentication) {
if (command.ignoreUnknownUsernames) {
return preventUserEnumeration(command.organization);
}
return {
error: "Username Password not allowed! Contact your administrator for more information.",
error: t("errors.localAuthenticationNotAllowed"),
};
}
@@ -497,7 +504,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
}
// user not found, check if register is enabled on instance / organization context
if (effectiveLoginSettings?.allowRegister && !effectiveLoginSettings?.allowUsernamePassword) {
if (effectiveLoginSettings?.allowRegister && !effectiveLoginSettings?.allowLocalAuthentication) {
console.log("redirecting to IDP (register allowed, password not allowed)");
const resp = await redirectUserToIDP(undefined, discoveredOrganization);
if (resp) {
@@ -506,7 +513,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
console.log("IDP redirect failed, returning user not found");
return preventUserEnumeration(discoveredOrganization);
} else if (effectiveLoginSettings?.allowRegister && effectiveLoginSettings?.allowUsernamePassword) {
} else if (effectiveLoginSettings?.allowRegister && effectiveLoginSettings?.allowLocalAuthentication) {
console.log("register and password both allowed");
// do not register user if ignoreUnknownUsernames is set
if (discoveredOrganization && !effectiveLoginSettings?.ignoreUnknownUsernames) {