Skip to content

Commit 0547013

Browse files
fix(auth): enforce zxcvbn strength check on password reset (#3721) (#3730)
* fix(auth): enforce zxcvbn strength check on password reset (#3721) The reset handler was calling hashPassword directly without first calling checkPassword, allowing weak passwords through the token-based reset flow. Add checkPassword call before hash (mirroring updatePassword), and add an integration test asserting weak passwords are rejected with 422. * test(auth): add checkPassword mock to silent-catch unit test The auth.service mock in auth.silent.catch.unit.tests.js was missing checkPassword, causing the reset handler to throw a TypeError when the fix calls AuthService.checkPassword(). Add it as a pass-through stub. * fix(auth): coerce newPassword to string before checkPassword in reset Mirrors the String() coercion already applied to token and email inputs in the same handler; guards against non-string body values reaching zxcvbn.
1 parent d42eb12 commit 0547013

3 files changed

Lines changed: 35 additions & 2 deletions

File tree

modules/auth/controllers/auth.password.controller.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,14 @@ const reset = async (req, res) => {
8989
// check input
9090
if (!req.body.token || !req.body.newPassword) return responses.error(res, 400, 'Bad Request', 'Password or Token fields must not be blank')();
9191
const token = String(req.body.token);
92+
const newPassword = String(req.body.newPassword);
9293
// get user by token, update with new password, login again
9394
try {
9495
user = await UserService.getBrut({ resetPasswordToken: token });
9596
if (!user || !user.email) return responses.error(res, 400, 'Bad Request', 'Password reset token is invalid or has expired.')();
97+
const checkedPassword = AuthService.checkPassword(newPassword);
9698
const edit = {
97-
password: await AuthService.hashPassword(req.body.newPassword),
99+
password: await AuthService.hashPassword(checkedPassword),
98100
resetPasswordToken: null,
99101
resetPasswordExpires: null,
100102
};

modules/auth/tests/auth.integration.tests.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,37 @@ describe('Auth integration tests:', () => {
12451245
}
12461246
});
12471247

1248+
test('should reject password reset with a weak password (zxcvbn strength gate)', async () => {
1249+
// Trigger forgot to generate a reset token (email send fails in test env, which is expected)
1250+
try {
1251+
await agent.post('/api/auth/forgot').send({ email: credentials[0].email }).expect(400);
1252+
} catch (err) {
1253+
console.log(err);
1254+
expect(err).toBeFalsy();
1255+
}
1256+
1257+
// Fetch the token directly via UserService
1258+
let resetToken;
1259+
try {
1260+
const userWithToken = await UserService.getBrut({ email: credentials[0].email });
1261+
resetToken = userWithToken.resetPasswordToken;
1262+
expect(resetToken).toBeDefined();
1263+
} catch (err) {
1264+
console.log(err);
1265+
expect(err).toBeFalsy();
1266+
}
1267+
1268+
// Attempt reset with a weak password — must be rejected with 422
1269+
try {
1270+
const result = await agent.post('/api/auth/reset').send({ token: resetToken, newPassword: 'password' }).expect(422);
1271+
expect(result.body.message).toBe('Unprocessable Entity');
1272+
expect(result.body.description).toBe('Password too weak.');
1273+
} catch (err) {
1274+
console.log(err);
1275+
expect(err).toBeFalsy();
1276+
}
1277+
});
1278+
12481279
test('should successfully reset password with a valid token', async () => {
12491280
// Trigger forgot to generate a reset token (email send fails in test env, which is expected)
12501281
try {

modules/auth/tests/auth.silent.catch.unit.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ describe('auth.password.controller silent-catch error logging:', () => {
184184
}));
185185

186186
jest.unstable_mockModule('../../../modules/auth/services/auth.service.js', () => ({
187-
default: { hashPassword: jest.fn().mockResolvedValue('hashed') },
187+
default: { checkPassword: jest.fn().mockReturnValue('NewP@ss1!'), hashPassword: jest.fn().mockResolvedValue('hashed') },
188188
}));
189189

190190
// sendMail rejects

0 commit comments

Comments
 (0)