From 8a17e2e32841193b4210649251a94829f9b6112f Mon Sep 17 00:00:00 2001
From: Aidan Daly <74743624+dalyaidan1@users.noreply.github.com>
Date: Sun, 24 May 2026 10:30:51 -0400
Subject: [PATCH 1/3] fix: validator callback types
---
src/Options/index.js | 3 +--
types/Options/index.d.ts | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/Options/index.js b/src/Options/index.js
index 68d1d61a94..38f175df92 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -664,11 +664,10 @@ export interface PasswordPolicyOptions {
If used in combination with `validatorCallback`, the password must pass both to be accepted. */
validatorPattern: ?string;
- /* */
/* Set a callback function to validate a password to be accepted.
If used in combination with `validatorPattern`, the password must pass both to be accepted. */
- validatorCallback: ?() => void;
+ validatorCallback: ?(password: string) => boolean;
/* Set the error message to be sent.
Default is `Password does not meet the Password Policy requirements.` */
diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts
index 6a8b1494ac..8c353eb191 100644
--- a/types/Options/index.d.ts
+++ b/types/Options/index.d.ts
@@ -226,7 +226,7 @@ export interface AccountLockoutOptions {
}
export interface PasswordPolicyOptions {
validatorPattern?: string;
- validatorCallback?: () => void;
+ validatorCallback?: (password: string) => boolean;
validationError?: string;
doNotAllowUsername?: boolean;
maxPasswordAge?: number;
From f900d40a6bd7de303d173e7da1537f5ce03d937e Mon Sep 17 00:00:00 2001
From: Aidan Daly <74743624+dalyaidan1@users.noreply.github.com>
Date: Sun, 24 May 2026 10:09:47 -0400
Subject: [PATCH 2/3] feat: async pw callback
---
src/Options/Definitions.js | 2 +-
src/Options/docs.js | 2 +-
src/Options/index.js | 3 ++-
src/RestWrite.js | 26 ++++++++++++++------------
types/Options/index.d.ts | 2 +-
5 files changed, 19 insertions(+), 16 deletions(-)
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index 77e5011354..162b00aeaf 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -1112,7 +1112,7 @@ module.exports.PasswordPolicyOptions = {
},
validatorCallback: {
env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK',
- help: 'Set a callback function to validate a password to be accepted.
If used in combination with `validatorPattern`, the password must pass both to be accepted.',
+ help: 'Set a callback function to validate a password to be accepted. Can return a boolean or `Promise`.
If used in combination with `validatorPattern`, the password must pass both to be accepted.',
},
validatorPattern: {
env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN',
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 09aff594d2..bdcd8f2f1f 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -256,7 +256,7 @@
* @property {Boolean} resetTokenReuseIfValid Set to `true` if a password reset token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.
Default is `false`.
* @property {Number} resetTokenValidityDuration Set the validity duration of the password reset token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.
For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).
Default is `undefined`.
* @property {String} validationError Set the error message to be sent.
Default is `Password does not meet the Password Policy requirements.`
- * @property {Function} validatorCallback Set a callback function to validate a password to be accepted.
If used in combination with `validatorPattern`, the password must pass both to be accepted.
+ * @property {Function} validatorCallback Set a callback function to validate a password to be accepted. Can return a boolean or `Promise`.
If used in combination with `validatorPattern`, the password must pass both to be accepted.
* @property {String} validatorPattern Set the regular expression validation pattern a password must match to be accepted.
If used in combination with `validatorCallback`, the password must pass both to be accepted.
*/
diff --git a/src/Options/index.js b/src/Options/index.js
index 38f175df92..b0c860cf3d 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -665,9 +665,10 @@ export interface PasswordPolicyOptions {
If used in combination with `validatorCallback`, the password must pass both to be accepted. */
validatorPattern: ?string;
/* Set a callback function to validate a password to be accepted.
+ Can return a `boolean` or `Promise`.
If used in combination with `validatorPattern`, the password must pass both to be accepted. */
- validatorCallback: ?(password: string) => boolean;
+ validatorCallback: ?(password: string) => boolean | Promise;
/* Set the error message to be sent.
Default is `Password does not meet the Password Policy requirements.` */
diff --git a/src/RestWrite.js b/src/RestWrite.js
index c6c8db969c..21a1ad88af 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -940,14 +940,13 @@ RestWrite.prototype._validateEmail = function () {
});
};
-RestWrite.prototype._validatePasswordPolicy = function () {
+RestWrite.prototype._validatePasswordPolicy = async function () {
if (!this.config.passwordPolicy) { return Promise.resolve(); }
- return this._validatePasswordRequirements().then(() => {
- return this._validatePasswordHistory();
- });
+ await this._validatePasswordRequirements();
+ return this._validatePasswordHistory();
};
-RestWrite.prototype._validatePasswordRequirements = function () {
+RestWrite.prototype._validatePasswordRequirements = async function () {
// check if the password conforms to the defined password policy if configured
// If we specified a custom error in our configuration use it.
// Example: "Passwords must include a Capital Letter, Lowercase Letter, and a number."
@@ -961,16 +960,19 @@ RestWrite.prototype._validatePasswordRequirements = function () {
: 'Password does not meet the Password Policy requirements.';
const containsUsernameError = 'Password cannot contain your username.';
- // check whether the password meets the password strength requirements
- if (
- (this.config.passwordPolicy.patternValidator &&
- !this.config.passwordPolicy.patternValidator(this.data.password)) ||
- (this.config.passwordPolicy.validatorCallback &&
- !this.config.passwordPolicy.validatorCallback(this.data.password))
- ) {
+ const patternValidator = this.config.passwordPolicy.patternValidator;
+ if (patternValidator && !patternValidator(this.data.password)) {
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError));
}
+ const validatorCallback = this.config.passwordPolicy.validatorCallback;
+ if (validatorCallback) {
+ const isValid = await Promise.resolve(validatorCallback(this.data.password));
+ if (!isValid) {
+ return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError));
+ }
+ }
+
// check whether password contain username
if (this.config.passwordPolicy.doNotAllowUsername === true) {
if (this.data.username) {
diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts
index 8c353eb191..18c53e01c4 100644
--- a/types/Options/index.d.ts
+++ b/types/Options/index.d.ts
@@ -226,7 +226,7 @@ export interface AccountLockoutOptions {
}
export interface PasswordPolicyOptions {
validatorPattern?: string;
- validatorCallback?: (password: string) => boolean;
+ validatorCallback?: (password: string) => boolean | Promise;
validationError?: string;
doNotAllowUsername?: boolean;
maxPasswordAge?: number;
From bd6230e779699652a3ec7e0f974686571398eef5 Mon Sep 17 00:00:00 2001
From: Aidan Daly <74743624+dalyaidan1@users.noreply.github.com>
Date: Sun, 24 May 2026 10:31:01 -0400
Subject: [PATCH 3/3] tests: async pw callback
---
spec/PasswordPolicy.spec.js | 92 +++++++++++++++++++++++++++++++++++++
1 file changed, 92 insertions(+)
diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js
index 27145015bc..18917332f3 100644
--- a/spec/PasswordPolicy.spec.js
+++ b/spec/PasswordPolicy.spec.js
@@ -512,6 +512,72 @@ describe('Password Policy: ', () => {
});
});
+ it('signup should fail if password does not conform to the policy enforced using async validatorCallback', done => {
+ const user = new Parse.User();
+ reconfigureServer({
+ appName: 'passwordPolicy',
+ passwordPolicy: {
+ validatorCallback: async password => password === 'valid',
+ },
+ publicServerURL: 'http://localhost:8378/1',
+ }).then(() => {
+ user.setUsername('user1');
+ user.setPassword('invalid');
+ user.set('email', 'user1@parse.com');
+ user
+ .signUp()
+ .then(() => {
+ fail('Should have failed as password does not conform to the policy.');
+ done();
+ })
+ .catch(error => {
+ expect(error.code).toEqual(142);
+ done();
+ });
+ });
+ });
+
+ it('signup should succeed if password conforms to the policy enforced using async validatorCallback', done => {
+ const user = new Parse.User();
+ reconfigureServer({
+ appName: 'passwordPolicy',
+ passwordPolicy: {
+ validatorCallback: async password => password === 'oneUpper',
+ },
+ publicServerURL: 'http://localhost:8378/1',
+ }).then(() => {
+ user.setUsername('user1');
+ user.setPassword('oneUpper');
+ user.set('email', 'user1@parse.com');
+ user
+ .signUp()
+ .then(() => {
+ Parse.User.logOut()
+ .then(() => {
+ Parse.User.logIn('user1', 'oneUpper')
+ .then(function () {
+ done();
+ })
+ .catch(err => {
+ jfail(err);
+ fail('Should be able to login');
+ done();
+ });
+ })
+ .catch(error => {
+ jfail(error);
+ fail('Logout should have succeeded');
+ done();
+ });
+ })
+ .catch(error => {
+ jfail(error);
+ fail('Should have succeeded as password conforms to the policy.');
+ done();
+ });
+ });
+ });
+
it('signup should fail if password does not match validatorPattern but succeeds validatorCallback', done => {
const user = new Parse.User();
reconfigureServer({
@@ -564,6 +630,32 @@ describe('Password Policy: ', () => {
});
});
+ it('signup should fail if password matches validatorPattern but fails async validatorCallback', done => {
+ const user = new Parse.User();
+ reconfigureServer({
+ appName: 'passwordPolicy',
+ passwordPolicy: {
+ validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter
+ validatorCallback: async () => false,
+ },
+ publicServerURL: 'http://localhost:8378/1',
+ }).then(() => {
+ user.setUsername('user1');
+ user.setPassword('oneUpper');
+ user.set('email', 'user1@parse.com');
+ user
+ .signUp()
+ .then(() => {
+ fail('Should have failed as password does not conform to the policy.');
+ done();
+ })
+ .catch(error => {
+ expect(error.code).toEqual(142);
+ done();
+ });
+ });
+ });
+
it('signup should succeed if password conforms to both validatorPattern and validatorCallback', done => {
const user = new Parse.User();
reconfigureServer({