fix: Conditional email verification not working in some cases if verifyUserEmails, preventLoginWithUnverifiedEmail set to functions (#8838)

This commit is contained in:
Manuel
2023-12-26 21:01:27 +01:00
committed by GitHub
parent f9dde4a9f8
commit 8e7a6b1480
5 changed files with 62 additions and 37 deletions

View File

@@ -389,7 +389,7 @@ describe('Email Verification Token Expiration: ', () => {
await user2.signUp(); await user2.signUp();
expect(user2.getSessionToken()).toBeUndefined(); expect(user2.getSessionToken()).toBeUndefined();
expect(sendEmailOptions).toBeDefined(); expect(sendEmailOptions).toBeDefined();
expect(verifySpy).toHaveBeenCalledTimes(4); expect(verifySpy).toHaveBeenCalledTimes(5);
}); });
it('can conditionally send user email verification', async () => { it('can conditionally send user email verification', async () => {

View File

@@ -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 () => { 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; let sendEmailOptions;
const emailAdapter = { const emailAdapter = {

View File

@@ -36,11 +36,10 @@ export class UserController extends AdaptableController {
} }
async setEmailVerifyToken(user, req, storage = {}) { async setEmailVerifyToken(user, req, storage = {}) {
let shouldSendEmail = this.shouldVerifyEmails; const shouldSendEmail =
if (typeof shouldSendEmail === 'function') { this.shouldVerifyEmails === true ||
const response = await Promise.resolve(shouldSendEmail(req)); (typeof this.shouldVerifyEmails === 'function' &&
shouldSendEmail = response !== false; (await Promise.resolve(this.shouldVerifyEmails(req))) === true);
}
if (!shouldSendEmail) { if (!shouldSendEmail) {
return false; return false;
} }

View File

@@ -930,31 +930,25 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () {
if (this.auth.user && this.data.authData) { if (this.auth.user && this.data.authData) {
return; return;
} }
if ( // If sign-up call
!this.storage.authProvider && // signup call, with if (!this.storage.authProvider) {
this.config.preventLoginWithUnverifiedEmail === true && // no login without verification // Create request object for verification functions
this.config.verifyUserEmails const { originalObject, updatedObject } = this.buildParseObjects();
) { const request = {
// verification is on original: originalObject,
this.storage.rejectSignup = true; object: updatedObject,
return; master: this.auth.isMaster,
} ip: this.config.ip,
if (!this.storage.authProvider && this.config.verifyUserEmails) { installationId: this.auth.installationId,
let shouldPreventUnverifedLogin = this.config.preventLoginWithUnverifiedEmail; };
if (typeof this.config.preventLoginWithUnverifiedEmail === 'function') { // Get verification conditions which can be booleans or functions; the purpose of this async/await
const { originalObject, updatedObject } = this.buildParseObjects(); // structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the
const request = { // conditional statement below, as a developer may decide to execute expensive operations in them
original: originalObject, const verifyUserEmails = async () => this.config.verifyUserEmails === true || (typeof this.config.verifyUserEmails === 'function' && await Promise.resolve(this.config.verifyUserEmails(request)) === true);
object: updatedObject, const preventLoginWithUnverifiedEmail = async () => this.config.preventLoginWithUnverifiedEmail === true || (typeof this.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(this.config.preventLoginWithUnverifiedEmail(request)) === true);
master: this.auth.isMaster, // If verification is required
ip: this.config.ip, if (await verifyUserEmails() && await preventLoginWithUnverifiedEmail()) {
installationId: this.auth.installationId, this.storage.rejectSignup = true;
};
shouldPreventUnverifedLogin = await Promise.resolve(
this.config.preventLoginWithUnverifiedEmail(request)
);
}
if (shouldPreventUnverifedLogin === true) {
return; return;
} }
} }

View File

@@ -126,7 +126,7 @@ export class UsersRouter extends ClassesRouter {
const accountLockoutPolicy = new AccountLockout(user, req.config); const accountLockoutPolicy = new AccountLockout(user, req.config);
return accountLockoutPolicy.handleLoginAttempt(isValidPassword); return accountLockoutPolicy.handleLoginAttempt(isValidPassword);
}) })
.then(() => { .then(async () => {
if (!isValidPassword) { if (!isValidPassword) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); 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) { if (!req.auth.isMaster && user.ACL && Object.keys(user.ACL).length == 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
} }
if ( // Create request object for verification functions
req.config.verifyUserEmails && const request = {
req.config.preventLoginWithUnverifiedEmail && master: req.auth.isMaster,
!user.emailVerified 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.'); throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.');
} }