Skip to content

Commit

Permalink
fix: Conditional email verification not working in some cases if `ver…
Browse files Browse the repository at this point in the history
…ifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions (#8838)
  • Loading branch information
mtrezza committed Dec 26, 2023
1 parent f9dde4a commit 8e7a6b1
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 37 deletions.
2 changes: 1 addition & 1 deletion spec/EmailVerificationToken.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
25 changes: 25 additions & 0 deletions spec/ValidationAndPasswordsReset.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
9 changes: 4 additions & 5 deletions src/Controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
44 changes: 19 additions & 25 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
19 changes: 13 additions & 6 deletions src/Routers/UsersRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand All @@ -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.');
}

Expand Down

0 comments on commit 8e7a6b1

Please sign in to comment.