Skip to content

Commit e5d610e

Browse files
authored
feat: Add Parse Server option resetPasswordSuccessOnInvalidEmail to choose success or error response on password reset with invalid email (#7551)
1 parent 5477848 commit e5d610e

File tree

6 files changed

+73
-16
lines changed

6 files changed

+73
-16
lines changed

spec/ValidationAndPasswordsReset.spec.js

+39
Original file line numberDiff line numberDiff line change
@@ -1082,4 +1082,43 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
10821082
done();
10831083
});
10841084
});
1085+
1086+
it('should throw on an invalid reset password', async () => {
1087+
await reconfigureServer({
1088+
appName: 'coolapp',
1089+
publicServerURL: 'http://localhost:1337/1',
1090+
emailAdapter: MockEmailAdapterWithOptions({
1091+
fromAddress: 'parse@example.com',
1092+
apiKey: 'k',
1093+
domain: 'd',
1094+
}),
1095+
passwordPolicy: {
1096+
resetPasswordSuccessOnInvalidEmail: false,
1097+
},
1098+
});
1099+
1100+
await expectAsync(Parse.User.requestPasswordReset('test@example.com')).toBeRejectedWith(
1101+
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'A user with that email does not exist.')
1102+
);
1103+
});
1104+
1105+
it('validate resetPasswordSuccessonInvalidEmail', async () => {
1106+
const invalidValues = [[], {}, 1, 'string'];
1107+
for (const value of invalidValues) {
1108+
await expectAsync(
1109+
reconfigureServer({
1110+
appName: 'coolapp',
1111+
publicServerURL: 'http://localhost:1337/1',
1112+
emailAdapter: MockEmailAdapterWithOptions({
1113+
fromAddress: 'parse@example.com',
1114+
apiKey: 'k',
1115+
domain: 'd',
1116+
}),
1117+
passwordPolicy: {
1118+
resetPasswordSuccessOnInvalidEmail: value,
1119+
},
1120+
})
1121+
).toBeRejectedWith('resetPasswordSuccessOnInvalidEmail must be a boolean value');
1122+
}
1123+
});
10851124
});

src/Config.js

+7
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,13 @@ export class Config {
376376
if (passwordPolicy.resetTokenReuseIfValid && !passwordPolicy.resetTokenValidityDuration) {
377377
throw 'You cannot use resetTokenReuseIfValid without resetTokenValidityDuration';
378378
}
379+
380+
if (
381+
passwordPolicy.resetPasswordSuccessOnInvalidEmail &&
382+
typeof passwordPolicy.resetPasswordSuccessOnInvalidEmail !== 'boolean'
383+
) {
384+
throw 'resetPasswordSuccessOnInvalidEmail must be a boolean value';
385+
}
379386
}
380387
}
381388

src/Options/Definitions.js

+7
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,13 @@ module.exports.PasswordPolicyOptions = {
907907
'Set the number of previous password that will not be allowed to be set as new password. If the option is not set or set to `0`, no previous passwords will be considered.<br><br>Valid values are >= `0` and <= `20`.<br>Default is `0`.',
908908
action: parsers.numberParser('maxPasswordHistory'),
909909
},
910+
resetPasswordSuccessOnInvalidEmail: {
911+
env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_PASSWORD_SUCCESS_ON_INVALID_EMAIL',
912+
help:
913+
'Set to `true` if a request to reset the password should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.<br><br>Default is `true`.',
914+
action: parsers.booleanParser,
915+
default: true,
916+
},
910917
resetTokenReuseIfValid: {
911918
env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID',
912919
help:

src/Options/docs.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,11 @@ export interface PasswordPolicyOptions {
525525
Default is `false`.
526526
:DEFAULT: false */
527527
resetTokenReuseIfValid: ?boolean;
528+
/* Set to `true` if a request to reset the password should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.
529+
<br><br>
530+
Default is `true`.
531+
:DEFAULT: true */
532+
resetPasswordSuccessOnInvalidEmail: ?boolean;
528533
}
529534

530535
export interface FileUploadOptions {

src/Routers/UsersRouter.js

+14-16
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ export class UsersRouter extends ClassesRouter {
414414
}
415415
}
416416

417-
handleResetRequest(req) {
417+
async handleResetRequest(req) {
418418
this._throwOnBadEmailConfig(req);
419419

420420
const { email } = req.body;
@@ -428,24 +428,22 @@ export class UsersRouter extends ClassesRouter {
428428
);
429429
}
430430
const userController = req.config.userController;
431-
return userController.sendPasswordResetEmail(email).then(
432-
() => {
433-
return Promise.resolve({
434-
response: {},
435-
});
436-
},
437-
err => {
438-
if (err.code === Parse.Error.OBJECT_NOT_FOUND) {
439-
// Return success so that this endpoint can't
440-
// be used to enumerate valid emails
441-
return Promise.resolve({
431+
try {
432+
await userController.sendPasswordResetEmail(email);
433+
return {
434+
response: {},
435+
};
436+
} catch (err) {
437+
if (err.code === Parse.Error.OBJECT_NOT_FOUND) {
438+
if (req.config.passwordPolicy?.resetPasswordSuccessOnInvalidEmail ?? true) {
439+
return {
442440
response: {},
443-
});
444-
} else {
445-
throw err;
441+
};
446442
}
443+
err.message = `A user with that email does not exist.`;
447444
}
448-
);
445+
throw err;
446+
}
449447
}
450448

451449
handleVerificationEmailRequest(req) {

0 commit comments

Comments
 (0)