From 820cd184dd4bee7e651cb5f31e83f433f5e87561 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 19 Dec 2023 02:16:53 +0100 Subject: [PATCH 01/10] Update RestWrite.js --- src/RestWrite.js | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index bbcc127f05..9c556051ac 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -930,31 +930,23 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { if (this.auth.user && this.data.authData) { return; } - if ( - !this.storage.authProvider && // signup call, with - this.config.preventLoginWithUnverifiedEmail === true && // no login without verification - this.config.verifyUserEmails - ) { - // verification is on - this.storage.rejectSignup = true; - return; - } - if (!this.storage.authProvider && this.config.verifyUserEmails) { - let shouldPreventUnverifedLogin = this.config.preventLoginWithUnverifiedEmail; - if (typeof this.config.preventLoginWithUnverifiedEmail === 'function') { - const { originalObject, updatedObject } = this.buildParseObjects(); - const request = { - original: originalObject, - object: updatedObject, - master: this.auth.isMaster, - ip: this.config.ip, - installationId: this.auth.installationId, - }; - shouldPreventUnverifedLogin = await Promise.resolve( - this.config.preventLoginWithUnverifiedEmail(request) - ); - } - if (shouldPreventUnverifedLogin === true) { + // If sign-up call + if (!this.storage.authProvider) { + // Create request object for the verification functions + const { originalObject, updatedObject } = this.buildParseObjects(); + const request = { + original: originalObject, + object: updatedObject, + master: this.auth.isMaster, + ip: this.config.ip, + installationId: this.auth.installationId, + }; + // Get verification conditions which can be boolean or functions + const verifyUserEmails = this.config.verifyUserEmails === true || (typeof this.config.verifyUserEmails === 'function' && await Promise.resolve(this.config.verifyUserEmails(request)) === true); + const preventLoginWithUnverifiedEmail = this.config.preventLoginWithUnverifiedEmail === true || (typeof this.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(this.config.preventLoginWithUnverifiedEmail(request)) === true); + // If verification is required + if (verifyUserEmails && preventLoginWithUnverifiedEmail) { + this.storage.rejectSignup = true; return; } } From b8ffbdeb0696198930b5c01ad503787a680ebc32 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 19 Dec 2023 03:18:11 +0100 Subject: [PATCH 02/10] Update EmailVerificationToken.spec.js --- spec/EmailVerificationToken.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 8501655143..8f3e0cea6e 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -382,6 +382,7 @@ describe('Email Verification Token Expiration: ', () => { expect(sendEmailOptions).toBeUndefined(); expect(user.getSessionToken()).toBeDefined(); expect(verifySpy).toHaveBeenCalledTimes(2); + expect(verifySpy).toHaveBeenCalledTimes(3); const user2 = new Parse.User(); user2.setUsername('email'); user2.setPassword('expiringToken'); @@ -389,7 +390,7 @@ describe('Email Verification Token Expiration: ', () => { await user2.signUp(); expect(user2.getSessionToken()).toBeUndefined(); expect(sendEmailOptions).toBeDefined(); - expect(verifySpy).toHaveBeenCalledTimes(4); + expect(verifySpy).toHaveBeenCalledTimes(6); }); it('can conditionally send user email verification', async () => { From e50b0893f3f004c4e62cdf2060c154ed8cab34b5 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 19 Dec 2023 03:18:22 +0100 Subject: [PATCH 03/10] Update EmailVerificationToken.spec.js --- spec/EmailVerificationToken.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 8f3e0cea6e..1cacd9912f 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -381,7 +381,6 @@ describe('Email Verification Token Expiration: ', () => { await user.signUp(); expect(sendEmailOptions).toBeUndefined(); expect(user.getSessionToken()).toBeDefined(); - expect(verifySpy).toHaveBeenCalledTimes(2); expect(verifySpy).toHaveBeenCalledTimes(3); const user2 = new Parse.User(); user2.setUsername('email'); From 0db82ffc926b75f475b2503aa29cdb7c1b0bfa3f Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 19 Dec 2023 03:22:29 +0100 Subject: [PATCH 04/10] code refactor --- src/Controllers/UserController.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 726dc279fa..69839aa87e 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -36,11 +36,10 @@ export class UserController extends AdaptableController { } async setEmailVerifyToken(user, req, storage = {}) { - let shouldSendEmail = this.shouldVerifyEmails; - if (typeof shouldSendEmail === 'function') { - const response = await Promise.resolve(shouldSendEmail(req)); - shouldSendEmail = response !== false; - } + const shouldSendEmail = + this.shouldVerifyEmails === true || + (typeof this.shouldVerifyEmails === 'function' && + (await Promise.resolve(this.shouldVerifyEmails(req))) === true); if (!shouldSendEmail) { return false; } From e9adb5be81046112ab340b4b84b0573d3014636a Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 19 Dec 2023 03:38:13 +0100 Subject: [PATCH 05/10] fix verification in UsersRouter --- src/RestWrite.js | 2 +- src/Routers/UsersRouter.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 9c556051ac..145a17be28 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -932,7 +932,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { } // If sign-up call if (!this.storage.authProvider) { - // Create request object for the verification functions + // Create request object for verification functions const { originalObject, updatedObject } = this.buildParseObjects(); const request = { original: originalObject, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index a0a801c09a..1f1d6a1d8e 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -126,7 +126,7 @@ export class UsersRouter extends ClassesRouter { const accountLockoutPolicy = new AccountLockout(user, req.config); return accountLockoutPolicy.handleLoginAttempt(isValidPassword); }) - .then(() => { + .then(async () => { if (!isValidPassword) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); } @@ -137,11 +137,15 @@ export class UsersRouter extends ClassesRouter { if (!req.auth.isMaster && user.ACL && Object.keys(user.ACL).length == 0) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); } - if ( - req.config.verifyUserEmails && - req.config.preventLoginWithUnverifiedEmail && - !user.emailVerified - ) { + // Create request object for verification functions + const request = { + master: req.auth.isMaster, + ip: req.config.ip, + installationId: req.auth.installationId, + }; + const verifyUserEmails = req.config.verifyUserEmails === true || (typeof req.config.verifyUserEmails === 'function' && await Promise.resolve(req.config.verifyUserEmails(request)) === true); + const preventLoginWithUnverifiedEmail = req.config.preventLoginWithUnverifiedEmail === true || (typeof req.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(req.config.preventLoginWithUnverifiedEmail(request)) === true); + if (verifyUserEmails && preventLoginWithUnverifiedEmail && !user.emailVerified) { throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); } From 44d02e468d08023d343b07e6453919bd21912f8a Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:13:37 +0100 Subject: [PATCH 06/10] add test --- spec/ValidationAndPasswordsReset.spec.js | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index ab944e14c1..2efae6505c 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -242,6 +242,31 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); + it('prevents user from signup and login if email is not verified and preventLoginWithUnverifiedEmail is set to function returning true', async () => { + await reconfigureServer({ + appName: 'test', + publicServerURL: 'http://localhost:1337/1', + verifyUserEmails: async () => true, + preventLoginWithUnverifiedEmail: async () => true, + preventSignupWithUnverifiedEmail: true, + emailAdapter: MockEmailAdapterWithOptions({ + fromAddress: 'parse@example.com', + apiKey: 'k', + domain: 'd', + }), + }); + + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'testInvalidConfig@parse.com'); + const signupRes = await user.signUp(null).catch(e => e); + expect(signupRes.message).toEqual('User email is not verified.'); + + const loginRes = await Parse.User.logIn('zxcv', 'asdf').catch(e => e); + expect(loginRes.message).toEqual('User email is not verified.'); + }); + it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', async () => { let sendEmailOptions; const emailAdapter = { From 516c6a466ebb3b947e7ea5092206572724907a3a Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 26 Dec 2023 19:24:10 +0100 Subject: [PATCH 07/10] add docs --- src/RestWrite.js | 2 +- src/Routers/UsersRouter.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 145a17be28..000001bfb0 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -941,7 +941,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { ip: this.config.ip, installationId: this.auth.installationId, }; - // Get verification conditions which can be boolean or functions + // Get verification conditions which can be booleans or functions const verifyUserEmails = this.config.verifyUserEmails === true || (typeof this.config.verifyUserEmails === 'function' && await Promise.resolve(this.config.verifyUserEmails(request)) === true); const preventLoginWithUnverifiedEmail = this.config.preventLoginWithUnverifiedEmail === true || (typeof this.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(this.config.preventLoginWithUnverifiedEmail(request)) === true); // If verification is required diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 1f1d6a1d8e..146375198d 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -143,6 +143,7 @@ export class UsersRouter extends ClassesRouter { ip: req.config.ip, installationId: req.auth.installationId, }; + // Get verification conditions which can be booleans or functions const verifyUserEmails = req.config.verifyUserEmails === true || (typeof req.config.verifyUserEmails === 'function' && await Promise.resolve(req.config.verifyUserEmails(request)) === true); const preventLoginWithUnverifiedEmail = req.config.preventLoginWithUnverifiedEmail === true || (typeof req.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(req.config.preventLoginWithUnverifiedEmail(request)) === true); if (verifyUserEmails && preventLoginWithUnverifiedEmail && !user.emailVerified) { From 741d8497c44556f1bc2f27819d9b7ea10713cd8d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 26 Dec 2023 19:33:19 +0100 Subject: [PATCH 08/10] optimization --- src/RestWrite.js | 10 ++++++---- src/Routers/UsersRouter.js | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 000001bfb0..49d587dc49 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -941,11 +941,13 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { ip: this.config.ip, installationId: this.auth.installationId, }; - // Get verification conditions which can be booleans or functions - const verifyUserEmails = this.config.verifyUserEmails === true || (typeof this.config.verifyUserEmails === 'function' && await Promise.resolve(this.config.verifyUserEmails(request)) === true); - const preventLoginWithUnverifiedEmail = this.config.preventLoginWithUnverifiedEmail === true || (typeof this.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(this.config.preventLoginWithUnverifiedEmail(request)) === true); + // Get verification conditions which can be booleans or functions; the purpose of this async/await + // structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the + // conditional statement below, as a developer may be executing database operations inside of them + const verifyUserEmails = async () => this.config.verifyUserEmails === true || (typeof this.config.verifyUserEmails === 'function' && await Promise.resolve(this.config.verifyUserEmails(request)) === true); + const preventLoginWithUnverifiedEmail = async () => this.config.preventLoginWithUnverifiedEmail === true || (typeof this.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(this.config.preventLoginWithUnverifiedEmail(request)) === true); // If verification is required - if (verifyUserEmails && preventLoginWithUnverifiedEmail) { + if (await verifyUserEmails() && await preventLoginWithUnverifiedEmail()) { this.storage.rejectSignup = true; return; } diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 146375198d..27824c255f 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -143,10 +143,12 @@ export class UsersRouter extends ClassesRouter { ip: req.config.ip, installationId: req.auth.installationId, }; - // Get verification conditions which can be booleans or functions - const verifyUserEmails = req.config.verifyUserEmails === true || (typeof req.config.verifyUserEmails === 'function' && await Promise.resolve(req.config.verifyUserEmails(request)) === true); - const preventLoginWithUnverifiedEmail = req.config.preventLoginWithUnverifiedEmail === true || (typeof req.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(req.config.preventLoginWithUnverifiedEmail(request)) === true); - if (verifyUserEmails && preventLoginWithUnverifiedEmail && !user.emailVerified) { + // Get verification conditions which can be booleans or functions; the purpose of this async/await + // structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the + // conditional statement below, as a developer may be executing database operations inside of them + const verifyUserEmails = async () => req.config.verifyUserEmails === true || (typeof req.config.verifyUserEmails === 'function' && await Promise.resolve(req.config.verifyUserEmails(request)) === true); + const preventLoginWithUnverifiedEmail = async () => req.config.preventLoginWithUnverifiedEmail === true || (typeof req.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(req.config.preventLoginWithUnverifiedEmail(request)) === true); + if (await verifyUserEmails() && await preventLoginWithUnverifiedEmail() && !user.emailVerified) { throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); } From 83bf9b5ff6f0c4b0a2ff686e20bf27f1997c498a Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 26 Dec 2023 19:36:28 +0100 Subject: [PATCH 09/10] docs --- src/RestWrite.js | 2 +- src/Routers/UsersRouter.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 49d587dc49..7243238cfa 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -943,7 +943,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { }; // Get verification conditions which can be booleans or functions; the purpose of this async/await // structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the - // conditional statement below, as a developer may be executing database operations inside of them + // conditional statement below, as a developer may decide to execute expensive operations in them const verifyUserEmails = async () => this.config.verifyUserEmails === true || (typeof this.config.verifyUserEmails === 'function' && await Promise.resolve(this.config.verifyUserEmails(request)) === true); const preventLoginWithUnverifiedEmail = async () => this.config.preventLoginWithUnverifiedEmail === true || (typeof this.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(this.config.preventLoginWithUnverifiedEmail(request)) === true); // If verification is required diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 27824c255f..63e3f60df2 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -145,7 +145,7 @@ export class UsersRouter extends ClassesRouter { }; // Get verification conditions which can be booleans or functions; the purpose of this async/await // structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the - // conditional statement below, as a developer may be executing database operations inside of them + // conditional statement below, as a developer may decide to execute expensive operations in them const verifyUserEmails = async () => req.config.verifyUserEmails === true || (typeof req.config.verifyUserEmails === 'function' && await Promise.resolve(req.config.verifyUserEmails(request)) === true); const preventLoginWithUnverifiedEmail = async () => req.config.preventLoginWithUnverifiedEmail === true || (typeof req.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(req.config.preventLoginWithUnverifiedEmail(request)) === true); if (await verifyUserEmails() && await preventLoginWithUnverifiedEmail() && !user.emailVerified) { From 281e68465d1a308b4ef3c98a0786dab09dc439d0 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 26 Dec 2023 19:57:28 +0100 Subject: [PATCH 10/10] fix test according to optimization --- spec/EmailVerificationToken.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 1cacd9912f..3963b2aac0 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -381,7 +381,7 @@ describe('Email Verification Token Expiration: ', () => { await user.signUp(); expect(sendEmailOptions).toBeUndefined(); expect(user.getSessionToken()).toBeDefined(); - expect(verifySpy).toHaveBeenCalledTimes(3); + expect(verifySpy).toHaveBeenCalledTimes(2); const user2 = new Parse.User(); user2.setUsername('email'); user2.setPassword('expiringToken'); @@ -389,7 +389,7 @@ describe('Email Verification Token Expiration: ', () => { await user2.signUp(); expect(user2.getSessionToken()).toBeUndefined(); expect(sendEmailOptions).toBeDefined(); - expect(verifySpy).toHaveBeenCalledTimes(6); + expect(verifySpy).toHaveBeenCalledTimes(5); }); it('can conditionally send user email verification', async () => {