feat: add current password field to password reset (#11534)

<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->

# Which Problems Are Solved

The password change flow in Login V2 does not ask the user to confirm
their current password before setting a new one.

# How the Problems Are Solved

- Added a "Current Password" field to the change password form
- The current password is verified via a session password check before
the new password is set
- If the current password is incorrect, the user sees a clear error
message
- Removed the previous 5-minute session freshness check, which is no
longer needed since the current password is verified inline

# Additional Changes

Added translations for the new field labels and error messages across
all supported languages

# Additional Context

- Closes #11533

---------

Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
Liam Neville
2026-02-18 10:05:26 +00:00
committed by GitHub
co-authored by Max Peintner
parent 11d48edd84
commit 64065bbe3c
16 changed files with 271 additions and 104 deletions
+3
View File
@@ -109,14 +109,17 @@
"description": "تعيين كلمة المرور لحسابك",
"submit": "متابعة",
"labels": {
"currentPassword": "كلمة المرور الحالية",
"newPassword": "كلمة المرور الجديدة",
"confirmPassword": "تأكيد كلمة المرور"
},
"required": {
"currentPassword": "عليك تقديم كلمة المرور الحالية!",
"newPassword": "عليك تقديم كلمة مرور جديدة!",
"confirmPassword": "هذا الحقل مطلوب"
},
"errors": {
"currentPasswordInvalid": "كلمة المرور الحالية غير صحيحة.",
"couldNotChangePassword": "تعذر تغيير كلمة المرور",
"couldNotVerifyPassword": "تعذر التحقق من كلمة المرور",
"unknownError": "خطأ غير معروف"
+3
View File
@@ -108,14 +108,17 @@
"description": "Legen Sie das Passwort für Ihr Konto fest",
"submit": "Weiter",
"labels": {
"currentPassword": "Aktuelles Passwort",
"newPassword": "Neues Passwort",
"confirmPassword": "Neues Passwort wiederholen"
},
"required": {
"currentPassword": "Bitte geben Sie Ihr aktuelles Passwort ein!",
"newPassword": "Bitte geben Sie ein neues Passwort ein!",
"confirmPassword": "Dieses Feld ist erforderlich"
},
"errors": {
"currentPasswordInvalid": "Das aktuelle Passwort ist falsch.",
"couldNotChangePassword": "Passwort konnte nicht geändert werden",
"couldNotVerifyPassword": "Passwort konnte nicht verifiziert werden",
"unknownError": "Unbekannter Fehler"
+3
View File
@@ -108,14 +108,17 @@
"description": "Set the password for your account",
"submit": "Continue",
"labels": {
"currentPassword": "Current Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password"
},
"required": {
"currentPassword": "You have to provide your current password!",
"newPassword": "You have to provide a new password!",
"confirmPassword": "This field is required"
},
"errors": {
"currentPasswordInvalid": "Current password is incorrect.",
"couldNotChangePassword": "Could not change password",
"couldNotVerifyPassword": "Could not verify password",
"unknownError": "Unknown error"
+3
View File
@@ -108,14 +108,17 @@
"description": "Establece la contraseña para tu cuenta",
"submit": "Continuar",
"labels": {
"currentPassword": "Contraseña actual",
"newPassword": "Nueva contraseña",
"confirmPassword": "Confirmar contraseña"
},
"required": {
"currentPassword": "¡Debes proporcionar tu contraseña actual!",
"newPassword": "¡Debes proporcionar una nueva contraseña!",
"confirmPassword": "Este campo es obligatorio"
},
"errors": {
"currentPasswordInvalid": "La contraseña actual es incorrecta.",
"couldNotChangePassword": "No se pudo cambiar la contraseña",
"couldNotVerifyPassword": "No se pudo verificar la contraseña",
"unknownError": "Error desconocido"
+3
View File
@@ -108,14 +108,17 @@
"description": "Définissez le mot de passe de votre compte",
"submit": "Continuer",
"labels": {
"currentPassword": "Mot de passe actuel",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmez le mot de passe"
},
"required": {
"currentPassword": "Vous devez fournir votre mot de passe actuel !",
"newPassword": "Vous devez fournir un nouveau mot de passe !",
"confirmPassword": "Ce champ est requis"
},
"errors": {
"currentPasswordInvalid": "Le mot de passe actuel est incorrect.",
"couldNotChangePassword": "Impossible de changer le mot de passe",
"couldNotVerifyPassword": "Impossible de vérifier le mot de passe",
"unknownError": "Erreur inconnue"
+3
View File
@@ -108,14 +108,17 @@
"description": "Imposta la password per il tuo account",
"submit": "Continua",
"labels": {
"currentPassword": "Password attuale",
"newPassword": "Nuova password",
"confirmPassword": "Conferma password"
},
"required": {
"currentPassword": "Devi fornire la tua password attuale!",
"newPassword": "Devi fornire una nuova password!",
"confirmPassword": "Questo campo è obbligatorio"
},
"errors": {
"currentPasswordInvalid": "La password attuale non è corretta.",
"couldNotChangePassword": "Impossibile modificare la password",
"couldNotVerifyPassword": "Impossibile verificare la password",
"unknownError": "Errore sconosciuto"
+3
View File
@@ -101,14 +101,17 @@
"description": "アカウントのパスワードを設定してください",
"submit": "続行",
"labels": {
"currentPassword": "現在のパスワード",
"newPassword": "新しいパスワード",
"confirmPassword": "パスワード確認"
},
"required": {
"currentPassword": "現在のパスワードを入力してください!",
"newPassword": "新しいパスワードを入力してください!",
"confirmPassword": "必須項目です"
},
"errors": {
"currentPasswordInvalid": "現在のパスワードが正しくありません。",
"couldNotChangePassword": "パスワードを変更できませんでした",
"couldNotVerifyPassword": "パスワードを確認できませんでした",
"unknownError": "不明なエラー"
+3
View File
@@ -108,14 +108,17 @@
"description": "Stel het wachtwoord voor jouw account in",
"submit": "Verder",
"labels": {
"currentPassword": "Huidig wachtwoord",
"newPassword": "Nieuw wachtwoord",
"confirmPassword": "Bevestig wachtwoord"
},
"required": {
"currentPassword": "Je moet je huidige wachtwoord opgeven!",
"newPassword": "Je moet een nieuw wachtwoord opgeven!",
"confirmPassword": "Dit veld is verplicht"
},
"errors": {
"currentPasswordInvalid": "Het huidige wachtwoord is onjuist.",
"couldNotChangePassword": "Kon wachtwoord niet wijzigen",
"couldNotVerifyPassword": "Kon wachtwoord niet verifiëren",
"unknownError": "Onbekende fout"
+3
View File
@@ -108,14 +108,17 @@
"description": "Ustaw nowe hasło dla swojego konta",
"submit": "Kontynuuj",
"labels": {
"currentPassword": "Aktualne hasło",
"newPassword": "Nowe hasło",
"confirmPassword": "Potwierdź nowe hasło"
},
"required": {
"currentPassword": "Musisz podać aktualne hasło!",
"newPassword": "Musisz podać nowe hasło!",
"confirmPassword": "To pole jest wymagane"
},
"errors": {
"currentPasswordInvalid": "Aktualne hasło jest nieprawidłowe.",
"couldNotChangePassword": "Nie można zmienić hasła",
"couldNotVerifyPassword": "Nie można zweryfikować hasła",
"unknownError": "Nieznany błąd"
+3
View File
@@ -108,14 +108,17 @@
"description": "Установите пароль для вашего аккаунта",
"submit": "Продолжить",
"labels": {
"currentPassword": "Текущий пароль",
"newPassword": "Новый пароль",
"confirmPassword": "Подтвердите пароль"
},
"required": {
"currentPassword": "Вы должны указать текущий пароль!",
"newPassword": "Вы должны указать новый пароль!",
"confirmPassword": "Это поле обязательно для заполнения"
},
"errors": {
"currentPasswordInvalid": "Текущий пароль неверен.",
"couldNotChangePassword": "Не удалось изменить пароль",
"couldNotVerifyPassword": "Не удалось проверить пароль",
"unknownError": "Неизвестная ошибка"
+3
View File
@@ -108,14 +108,17 @@
"description": "Hesabınız için şifre belirleyin",
"submit": "Devam",
"labels": {
"currentPassword": "Mevcut Şifre",
"newPassword": "Yeni Şifre",
"confirmPassword": "Şifreyi Onayla"
},
"required": {
"currentPassword": "Mevcut şifrenizi girmelisiniz!",
"newPassword": "Yeni bir şifre sağlamalısınız!",
"confirmPassword": "Bu alan zorunludur"
},
"errors": {
"currentPasswordInvalid": "Mevcut şifre yanlış.",
"couldNotChangePassword": "Şifre değiştirilemedi",
"couldNotVerifyPassword": "Şifre doğrulanamadı",
"unknownError": "Bilinmeyen hata"
+3
View File
@@ -108,14 +108,17 @@
"description": "Встановіть пароль для свого облікового запису",
"submit": "Продовжити",
"labels": {
"currentPassword": "Поточний пароль",
"newPassword": "Новий пароль",
"confirmPassword": "Підтвердити пароль"
},
"required": {
"currentPassword": "Ви повинні вказати поточний пароль!",
"newPassword": "Ви повинні вказати новий пароль!",
"confirmPassword": "Це поле обов'язкове"
},
"errors": {
"currentPasswordInvalid": "Поточний пароль невірний.",
"couldNotChangePassword": "Не вдалося змінити пароль",
"couldNotVerifyPassword": "Не вдалося перевірити пароль",
"unknownError": "Невідома помилка"
+3
View File
@@ -108,14 +108,17 @@
"description": "为您的账户设置密码",
"submit": "继续",
"labels": {
"currentPassword": "当前密码",
"newPassword": "新密码",
"confirmPassword": "确认密码"
},
"required": {
"currentPassword": "必须提供当前密码!",
"newPassword": "必须提供新密码!",
"confirmPassword": "此字段为必填项"
},
"errors": {
"currentPasswordInvalid": "当前密码不正确。",
"couldNotChangePassword": "无法更改密码",
"couldNotVerifyPassword": "无法验证密码",
"unknownError": "未知错误"
@@ -21,6 +21,7 @@ import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
currentPassword: string;
password: string;
confirmPassword: string;
}
@@ -40,8 +41,9 @@ export function ChangePasswordForm({ passwordComplexitySettings, sessionId, logi
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
mode: "onChange",
defaultValues: {
currentPassword: "",
password: "",
comfirmPassword: "",
confirmPassword: "",
},
});
@@ -56,6 +58,7 @@ export function ChangePasswordForm({ passwordComplexitySettings, sessionId, logi
const changeResponse = await checkSessionAndSetPassword({
sessionId,
currentPassword: values.currentPassword,
password: values.password,
}).catch(() => {
setError(t("change.errors.couldNotChangePassword"));
@@ -122,6 +125,19 @@ export function ChangePasswordForm({ passwordComplexitySettings, sessionId, logi
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className="mb-4 grid grid-cols-1 gap-4 pt-4">
<div className="">
<TextInput
type="password"
autoComplete="current-password"
required
{...register("currentPassword", {
required: t("change.required.currentPassword"),
})}
label={t("change.labels.currentPassword")}
error={errors.currentPassword?.message as string}
data-testid="password-change-current-text-input"
/>
</div>
<div className="">
<TextInput
type="password"
+158 -87
View File
@@ -69,7 +69,9 @@ describe("checkSessionAndSetPassword", () => {
let mockGetSession: any;
let mockListAuthenticationMethodTypes: any;
let mockGetLoginSettings: any;
let mockSetPassword: any; // Service account
let mockGetLockoutSettings: any;
let mockSetPassword: any;
let mockSetSessionAndUpdateCookie: any;
beforeEach(async () => {
vi.clearAllMocks();
@@ -77,7 +79,9 @@ describe("checkSessionAndSetPassword", () => {
const { headers } = await import("next/headers");
const { getServiceConfig } = await import("../service-url");
const { getSessionCookieById } = await import("../cookies");
const { getSession, listAuthenticationMethodTypes, getLoginSettings, setPassword } = await import("../zitadel");
const { getSession, listAuthenticationMethodTypes, getLoginSettings, getLockoutSettings, setPassword } =
await import("../zitadel");
const { setSessionAndUpdateCookie } = await import("./cookie");
mockHeaders = vi.mocked(headers);
mockGetServiceConfig = vi.mocked(getServiceConfig);
@@ -85,18 +89,20 @@ describe("checkSessionAndSetPassword", () => {
mockGetSession = vi.mocked(getSession);
mockListAuthenticationMethodTypes = vi.mocked(listAuthenticationMethodTypes);
mockGetLoginSettings = vi.mocked(getLoginSettings);
mockGetLockoutSettings = vi.mocked(getLockoutSettings);
mockSetPassword = vi.mocked(setPassword);
mockSetSessionAndUpdateCookie = vi.mocked(setSessionAndUpdateCookie);
mockHeaders.mockResolvedValue({});
mockGetServiceConfig.mockReturnValue({ serviceConfig: { baseUrl: "https://api.example.com" } });
// Default session setup
mockGetSessionCookieById.mockResolvedValue({ id: "session123", token: "token123" });
mockGetSessionCookieById.mockResolvedValue({ id: "session123", token: "token123", organization: "org123" });
mockGetSession.mockResolvedValue({
session: {
factors: {
user: { id: "user123", organizationId: "org123" },
password: { verifiedAt: { seconds: Math.floor(Date.now() / 1000) } }, // Password verified recently
password: { verifiedAt: { seconds: Math.floor(Date.now() / 1000) } },
},
},
});
@@ -106,111 +112,176 @@ describe("checkSessionAndSetPassword", () => {
authMethodTypes: [AuthenticationMethodType.PASSWORD],
});
// Default: No forced MFA
mockGetLoginSettings.mockResolvedValue({ forceMfa: false });
// Default: login settings with password check lifetime
mockGetLoginSettings.mockResolvedValue({
forceMfa: false,
passwordCheckLifetime: { seconds: BigInt(60 * 60 * 24), nanos: 0 },
});
// Mock user service client
// Default: current password verification succeeds
mockSetSessionAndUpdateCookie.mockResolvedValue({});
mockSetPassword.mockResolvedValue({});
});
test("should use service account when no MFA is configured", async () => {
await checkSessionAndSetPassword({ sessionId: "session123", password: "newpassword" });
test("should verify current password and set new password", async () => {
await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "oldpassword",
password: "newpassword",
});
expect(mockSetSessionAndUpdateCookie).toHaveBeenCalledWith(
expect.objectContaining({
recentCookie: expect.objectContaining({ id: "session123" }),
}),
);
expect(mockSetPassword).toHaveBeenCalled();
});
test("should use service account when MFA is configured but NOT verified in session", async () => {
// User has TOTP configured
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.TOTP],
test("should return error when current password is incorrect", async () => {
mockSetSessionAndUpdateCookie.mockRejectedValue(new Error("invalid password"));
const result = await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "wrongpassword",
password: "newpassword",
});
// Session only has password verified (no OTP)
const now = Math.floor(Date.now() / 1000);
mockGetSession.mockResolvedValue({
session: {
factors: {
user: { id: "user123", organizationId: "org123" },
password: { verifiedAt: { seconds: now - 60 } }, // Verified 1 minute ago
// otp missing
},
},
});
await checkSessionAndSetPassword({ sessionId: "session123", password: "newpassword" });
expect(mockSetPassword).toHaveBeenCalled();
});
test("should use service account when MFA is configured AND verified in session", async () => {
// User has TOTP configured
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.TOTP],
});
// Session has both password and OTP verified
mockGetSession.mockResolvedValue({
session: {
factors: {
user: { id: "user123", organizationId: "org123" },
password: { verifiedAt: { seconds: Math.floor(Date.now() / 1000) } },
totp: { verifiedAt: { seconds: Math.floor(Date.now() / 1000) } }, // Verified
},
},
});
await checkSessionAndSetPassword({ sessionId: "session123", password: "newpassword" });
expect(mockSetPassword).toHaveBeenCalled();
});
test("should fail when MFA is configured but not verified, and password verification is too old", async () => {
// User has TOTP configured
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.TOTP],
});
// Session has password verified 10 minutes ago (600 seconds)
const now = Math.floor(Date.now() / 1000);
mockGetSession.mockResolvedValue({
session: {
factors: {
user: { id: "user123", organizationId: "org123" },
password: { verifiedAt: { seconds: now - 600 } },
// otp missing
},
},
});
const result = await checkSessionAndSetPassword({ sessionId: "session123", password: "newpassword" });
expect(result).toEqual({ error: "errors.passwordVerificationTooOld" });
expect(result).toEqual({ error: "change.errors.currentPasswordInvalid" });
expect(mockSetPassword).not.toHaveBeenCalled();
});
test("should use service account when MFA is configured but not verified, and password verification is recent", async () => {
// User has TOTP configured
mockListAuthenticationMethodTypes.mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.TOTP],
test("should return generic error when current password is incorrect and ignoreUnknownUsernames is true", async () => {
mockGetLoginSettings.mockResolvedValue({
forceMfa: false,
passwordCheckLifetime: { seconds: BigInt(60 * 60 * 24), nanos: 0 },
ignoreUnknownUsernames: true,
});
mockSetSessionAndUpdateCookie.mockRejectedValue(new Error("invalid password"));
const result = await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "wrongpassword",
password: "newpassword",
});
// Session has password verified 1 minute ago (60 seconds)
const now = Math.floor(Date.now() / 1000);
expect(result).toEqual({ error: "change.errors.couldNotVerifyPassword" });
expect(mockSetPassword).not.toHaveBeenCalled();
});
test("should pass requestId from session cookie to setSessionAndUpdateCookie", async () => {
mockGetSessionCookieById.mockResolvedValue({
id: "session123",
token: "token123",
organization: "org123",
requestId: "oidc-request-456",
});
await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "oldpassword",
password: "newpassword",
});
expect(mockSetSessionAndUpdateCookie).toHaveBeenCalledWith(
expect.objectContaining({
requestId: "oidc-request-456",
}),
);
});
test("should return lockout error with attempt counts when failedAttempts error is thrown", async () => {
mockSetSessionAndUpdateCookie.mockRejectedValue({ failedAttempts: 3 });
mockGetLockoutSettings.mockResolvedValue({ maxPasswordAttempts: BigInt(5) });
const result = await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "wrongpassword",
password: "newpassword",
});
expect(result).toEqual({ error: "errors.failedToAuthenticate" });
expect(mockGetLockoutSettings).toHaveBeenCalled();
expect(mockSetPassword).not.toHaveBeenCalled();
});
test("should return generic error for failedAttempts when ignoreUnknownUsernames is true", async () => {
mockGetLoginSettings.mockResolvedValue({
forceMfa: false,
passwordCheckLifetime: { seconds: BigInt(60 * 60 * 24), nanos: 0 },
ignoreUnknownUsernames: true,
});
mockSetSessionAndUpdateCookie.mockRejectedValue({ failedAttempts: 3 });
const result = await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "wrongpassword",
password: "newpassword",
});
expect(result).toEqual({ error: "errors.failedToAuthenticateNoLimit" });
expect(mockGetLockoutSettings).not.toHaveBeenCalled();
expect(mockSetPassword).not.toHaveBeenCalled();
});
test("should return error when session cookie not found", async () => {
mockGetSessionCookieById.mockResolvedValue(null);
const result = await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "oldpassword",
password: "newpassword",
});
expect(result).toEqual({ error: "Could not load session cookie" });
expect(mockSetSessionAndUpdateCookie).not.toHaveBeenCalled();
expect(mockSetPassword).not.toHaveBeenCalled();
});
test("should return error when session has no user", async () => {
mockGetSession.mockResolvedValue({
session: {
factors: {
user: { id: "user123", organizationId: "org123" },
password: { verifiedAt: { seconds: now - 60 } },
// otp missing
},
},
session: { factors: {} },
});
await checkSessionAndSetPassword({ sessionId: "session123", password: "newpassword" });
const result = await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "oldpassword",
password: "newpassword",
});
expect(result).toEqual({ error: "errors.couldNotLoadSession" });
expect(mockSetPassword).not.toHaveBeenCalled();
});
test("should use default lifetime when login settings have no passwordCheckLifetime", async () => {
mockGetLoginSettings.mockResolvedValue({ forceMfa: false });
await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "oldpassword",
password: "newpassword",
});
expect(mockSetSessionAndUpdateCookie).toHaveBeenCalledWith(
expect.objectContaining({
lifetime: expect.objectContaining({ seconds: BigInt(60 * 60 * 24) }),
}),
);
expect(mockSetPassword).toHaveBeenCalled();
});
test("should handle setPassword failure with failed precondition", async () => {
mockSetPassword.mockRejectedValue({ code: 9, message: "User is not yet initialized" });
const result = await checkSessionAndSetPassword({
sessionId: "session123",
currentPassword: "oldpassword",
password: "newpassword",
});
expect(result).toEqual({ error: "errors.failedPrecondition" });
});
});
describe("sendPassword", () => {
+57 -16
View File
@@ -13,7 +13,7 @@ import {
setPassword,
setUserPassword,
} from "@/lib/zitadel";
import { create, Duration, timestampDate } from "@zitadel/client";
import { create, Duration } from "@zitadel/client";
import { Checks, ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
@@ -448,10 +448,15 @@ export async function changePassword(command: { code?: string; userId: string; p
type CheckSessionAndSetPasswordCommand = {
sessionId: string;
currentPassword: string;
password: string;
};
export async function checkSessionAndSetPassword({ sessionId, password }: CheckSessionAndSetPasswordCommand) {
export async function checkSessionAndSetPassword({
sessionId,
currentPassword,
password,
}: CheckSessionAndSetPasswordCommand) {
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("password");
@@ -479,6 +484,56 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
return { error: t("errors.couldNotLoadSession") };
}
const loginSettings = await getLoginSettings({
serviceConfig,
organization: sessionCookie.organization,
});
let lifetime = loginSettings?.passwordCheckLifetime;
if (!lifetime || !lifetime.seconds) {
lifetime = {
seconds: BigInt(60 * 60 * 24),
nanos: 0,
} as Duration;
}
const checks = create(ChecksSchema, {
password: { password: currentPassword },
});
try {
await setSessionAndUpdateCookie({
recentCookie: sessionCookie,
checks,
lifetime,
requestId: sessionCookie.requestId,
});
} catch (error: any) {
if ("failedAttempts" in error && error.failedAttempts) {
if (loginSettings?.ignoreUnknownUsernames) {
return { error: t("errors.failedToAuthenticateNoLimit") };
}
const lockoutSettings = await getLockoutSettings({ serviceConfig, orgId: sessionCookie.organization });
const hasLimit =
lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
const locked = hasLimit && error.failedAttempts >= lockoutSettings?.maxPasswordAttempts;
const messageKey = hasLimit ? "errors.failedToAuthenticate" : "errors.failedToAuthenticateNoLimit";
return {
error: t(messageKey, {
failedAttempts: error.failedAttempts,
maxPasswordAttempts: hasLimit ? (lockoutSettings?.maxPasswordAttempts).toString() : "?",
lockoutMessage: locked ? t("errors.accountLockedContactAdmin") : "",
}),
};
}
if (loginSettings?.ignoreUnknownUsernames) {
return { error: t("change.errors.couldNotVerifyPassword") };
}
return { error: t("change.errors.currentPasswordInvalid") };
}
const payload = create(SetPasswordRequestSchema, {
userId: session.factors.user.id,
newPassword: {
@@ -486,20 +541,6 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
},
});
// check if the password factor is set and not older than 5 minutes
const passwordVerifiedAt = session.factors?.password?.verifiedAt;
if (!passwordVerifiedAt) {
return { error: t("errors.passwordVerificationMissing") };
}
const passwordVerifiedDate = timestampDate(passwordVerifiedAt);
const now = new Date();
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
if (passwordVerifiedDate < fiveMinutesAgo) {
return { error: t("errors.passwordVerificationTooOld") };
}
return setPassword({ serviceConfig, payload }).catch((error) => {
// throw error if failed precondition (ex. User is not yet initialized)
if (error.code === 9 && error.message) {