From 8e7a6b1480c0117e6c73e7adc5a6619115a04e85 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Tue, 26 Dec 2023 21:01:27 +0100 Subject: [PATCH] fix: Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions (#8838) --- spec/EmailVerificationToken.spec.js | 2 +- spec/ValidationAndPasswordsReset.spec.js | 25 ++++++++++++++ src/Controllers/UserController.js | 9 +++-- src/RestWrite.js | 44 ++++++++++-------------- src/Routers/UsersRouter.js | 19 ++++++---- 5 files changed, 62 insertions(+), 37 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 8501655143..3963b2aac0 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -389,7 +389,7 @@ describe('Email Verification Token Expiration: ', () => { await user2.signUp(); expect(user2.getSessionToken()).toBeUndefined(); expect(sendEmailOptions).toBeDefined(); - expect(verifySpy).toHaveBeenCalledTimes(4); + expect(verifySpy).toHaveBeenCalledTimes(5); }); it('can conditionally send user email verification', async () => { 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 = { 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; } diff --git a/src/RestWrite.js b/src/RestWrite.js index bbcc127f05..7243238cfa 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -930,31 +930,25 @@ 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 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 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 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 + if (await verifyUserEmails() && await preventLoginWithUnverifiedEmail()) { + this.storage.rejectSignup = true; return; } } diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index a0a801c09a..63e3f60df2 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,18 @@ 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, + }; + // 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 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) { throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); }