From 04b4912d59d6af64dedf15daa3e0cb729bcca5bd Mon Sep 17 00:00:00 2001 From: Julius Jung Date: Sat, 17 Mar 2018 17:13:54 -0400 Subject: [PATCH] Fix password reset when querying for emails with upcase characters (fixes #9059) (#9707) * downcase updating an email to be consistent with creating * add tests to ensure downcase of email for create/update * create migration to downcase existing User objects * delete 'only' * change gmail to example * add trailing comma from lint error * search for emails with at least one capital letter * fix query in order to search for any email with at least one capital letter * batch process effected users with at least one capital in email * update script for batch process effected users --- migrations/20171211_sanitize_emails.js | 88 +++++++++++++++++++ .../user/auth/POST-register_local.test.js | 15 ++++ .../user/auth/PUT-user_update_email.test.js | 7 +- website/server/controllers/api-v3/auth.js | 4 +- 4 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 migrations/20171211_sanitize_emails.js diff --git a/migrations/20171211_sanitize_emails.js b/migrations/20171211_sanitize_emails.js new file mode 100644 index 0000000000..cd6bcab960 --- /dev/null +++ b/migrations/20171211_sanitize_emails.js @@ -0,0 +1,88 @@ +var migrationName = '20171211_sanitize_emails.js'; +var authorName = 'Julius'; // in case script author needs to know when their ... +var authorUuid = 'dd16c270-1d6d-44bd-b4f9-737342e79be6'; //... own data is done + +/* + User creation saves email as lowercase, but updating an email did not. + Run this script to ensure all lowercased emails in db AFTER fix for updating emails is implemented. + This will fix inconsistent querying for an email when attempting to password reset. +*/ + +var monk = require('monk'); +var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE +var dbUsers = monk(connectionString).get('users', { castIds: false }); + +function processUsers(lastId) { + var query = { + 'auth.local.email': /[A-Z]/ + }; + + if (lastId) { + query._id = { + $gt: lastId + } + } + + dbUsers.find(query, { + sort: {_id: 1}, + limit: 250, + fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data) + 'auth.local.email' + ], + }) + .then(updateUsers) + .catch(function (err) { + console.log(err); + return exiting(1, 'ERROR! ' + err); + }); +} + +var progressCount = 1000; +var count = 0; + +function updateUsers (users) { + if (!users || users.length === 0) { + console.warn('All appropriate users found and modified.'); + displayData(); + return; + } + + var userPromises = users.map(updateUser); + var lastUser = users[users.length - 1]; + + return Promise.all(userPromises) + .then(function () { + processUsers(lastUser._id); + }); +} + +function updateUser (user) { + count++; + + var push; + var set = { + 'auth.local.email': user.auth.local.email.toLowerCase() + }; + + dbUsers.update({_id: user._id}, {$set: set}); + + if (count % progressCount == 0) console.warn(count + ' ' + user._id); + if (user._id == authorUuid) console.warn(authorName + ' processed'); +} + +function displayData() { + console.warn('\n' + count + ' users processed\n'); + return exiting(0); +} + +function exiting(code, msg) { + code = code || 0; // 0 = success + if (code && !msg) { msg = 'ERROR!'; } + if (msg) { + if (code) { console.error(msg); } + else { console.log( msg); } + } + process.exit(code); +} + +module.exports = processUsers; diff --git a/test/api/v3/integration/user/auth/POST-register_local.test.js b/test/api/v3/integration/user/auth/POST-register_local.test.js index 1ad091a79e..643ef3d9c2 100644 --- a/test/api/v3/integration/user/auth/POST-register_local.test.js +++ b/test/api/v3/integration/user/auth/POST-register_local.test.js @@ -357,6 +357,21 @@ describe('POST /user/auth/local/register', () => { }); }); + it('sanitizes email params to a lowercase string before creating the user', async () => { + let username = generateRandomUserName(); + let email = 'ISANEmAiL@ExAmPle.coM'; + let password = 'password'; + + let user = await api.post('/user/auth/local/register', { + username, + email, + password, + confirmPassword: password, + }); + + expect(user.auth.local.email).to.equal(email.toLowerCase()); + }); + it('fails on a habitica.com email', async () => { let username = generateRandomUserName(); let email = `${username}@habitica.com`; diff --git a/test/api/v3/integration/user/auth/PUT-user_update_email.test.js b/test/api/v3/integration/user/auth/PUT-user_update_email.test.js index d893f5ac80..d61a71c130 100644 --- a/test/api/v3/integration/user/auth/PUT-user_update_email.test.js +++ b/test/api/v3/integration/user/auth/PUT-user_update_email.test.js @@ -13,7 +13,7 @@ import nconf from 'nconf'; const ENDPOINT = '/user/auth/update-email'; describe('PUT /user/auth/update-email', () => { - let newEmail = 'some-new-email_2@example.net'; + let newEmail = 'SOmE-nEw-emAIl_2@example.net'; let oldPassword = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js context('Local Authenticaion User', async () => { @@ -53,14 +53,15 @@ describe('PUT /user/auth/update-email', () => { }); it('changes email if new email and existing password are provided', async () => { + let lowerCaseNewEmail = newEmail.toLowerCase(); let response = await user.put(ENDPOINT, { newEmail, password: oldPassword, }); - expect(response).to.eql({ email: 'some-new-email_2@example.net' }); + expect(response.email).to.eql(lowerCaseNewEmail); await user.sync(); - expect(user.auth.local.email).to.eql(newEmail); + expect(user.auth.local.email).to.eql(lowerCaseNewEmail); }); it('rejects if email is already taken', async () => { diff --git a/website/server/controllers/api-v3/auth.js b/website/server/controllers/api-v3/auth.js index 542e77b081..ae62382f48 100644 --- a/website/server/controllers/api-v3/auth.js +++ b/website/server/controllers/api-v3/auth.js @@ -629,7 +629,7 @@ api.updateEmail = { if (validationErrors) throw validationErrors; let emailAlreadyInUse = await User.findOne({ - 'auth.local.email': req.body.newEmail, + 'auth.local.email': req.body.newEmail.toLowerCase(), }).select({_id: 1}).lean().exec(); if (emailAlreadyInUse) throw new NotAuthorized(res.t('cannotFulfillReq', { techAssistanceEmail: TECH_ASSISTANCE_EMAIL })); @@ -643,7 +643,7 @@ api.updateEmail = { await passwordUtils.convertToBcrypt(user, password); } - user.auth.local.email = req.body.newEmail; + user.auth.local.email = req.body.newEmail.toLowerCase(); await user.save(); return res.respond(200, { email: user.auth.local.email });