From 78c67d4c6ea67675c52947749b3ef89623fd0990 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 30 Mar 2023 13:18:33 +1100 Subject: [PATCH 1/5] fix: remove username from verification emails --- spec/AccountLockoutPolicy.spec.js | 4 +- spec/EmailVerificationToken.spec.js | 8 +- spec/PagesRouter.spec.js | 30 ++---- spec/PasswordPolicy.spec.js | 42 ++++----- spec/PublicAPI.spec.js | 26 +----- spec/RegexVulnerabilities.spec.js | 8 +- spec/UserController.spec.js | 4 +- spec/ValidationAndPasswordsReset.spec.js | 29 +++--- src/Controllers/UserController.js | 114 ++++++++++------------- src/GraphQL/loaders/usersMutations.js | 7 +- src/Routers/PagesRouter.js | 43 +++------ src/Routers/PublicAPIRouter.js | 33 +++---- 12 files changed, 132 insertions(+), 216 deletions(-) diff --git a/spec/AccountLockoutPolicy.spec.js b/spec/AccountLockoutPolicy.spec.js index 43212d0e69..da8048adab 100644 --- a/spec/AccountLockoutPolicy.spec.js +++ b/spec/AccountLockoutPolicy.spec.js @@ -419,7 +419,7 @@ describe('lockout with password reset option', () => { await request({ method: 'POST', url: `${config.publicServerURL}/apps/test/request_password_reset`, - body: `new_password=${newPassword}&token=${token}&username=${username}`, + body: `new_password=${newPassword}&token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -454,7 +454,7 @@ describe('lockout with password reset option', () => { await request({ method: 'POST', url: `${config.publicServerURL}/apps/test/request_password_reset`, - body: `new_password=${newPassword}&token=${token}&username=${username}`, + body: `new_password=${newPassword}&token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index e21a049719..2b080c139a 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -39,7 +39,7 @@ describe('Email Verification Token Expiration: ', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' ); done(); }); @@ -133,7 +133,7 @@ describe('Email Verification Token Expiration: ', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' ); done(); }); @@ -392,7 +392,7 @@ describe('Email Verification Token Expiration: ', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' ); done(); }); @@ -445,7 +445,7 @@ describe('Email Verification Token Expiration: ', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' ); done(); }); diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index e50144f1fe..319d19c1b8 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -108,7 +108,7 @@ describe('Pages Router', () => { const res = await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=43634643&username=username`, + body: `new_password=user1&token=43634643`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -124,7 +124,7 @@ describe('Pages Router', () => { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=&token=132414&username=Johnny`, + body: `new_password=&token=132414`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -137,30 +137,12 @@ describe('Pages Router', () => { } }); - it('request_password_reset: responds with AJAX error on missing username', async () => { - try { - await request({ - method: 'POST', - url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=43634643&username=`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest', - }, - followRedirects: false, - }); - } catch (error) { - expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); - } - }); - it('request_password_reset: responds with AJAX error on missing token', async () => { try { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=&username=Johnny`, + body: `new_password=user1&token=`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -577,7 +559,7 @@ describe('Pages Router', () => { spyOnProperty(Page.prototype, 'defaultFile').and.returnValue(jsonPageFile); const response = await request({ - url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=${exampleLocale}`, + url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=${exampleLocale}`, followRedirects: false, }).catch(e => e); expect(response.status).toEqual(200); @@ -626,7 +608,7 @@ describe('Pages Router', () => { await reconfigureServer(config); const response = await request({ url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT', followRedirects: false, method: 'POST', }); @@ -640,7 +622,7 @@ describe('Pages Router', () => { await reconfigureServer(config); const response = await request({ url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT', followRedirects: false, method: 'GET', }); diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 4ea6ed2002..d6f50279f4 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -107,7 +107,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/; expect(response.text.match(re)).not.toBe(null); done(); }) @@ -622,7 +622,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -634,7 +634,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=has2init&token=${token}&username=user1`, + body: `new_password=has2init&token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -645,7 +645,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' ); Parse.User.logIn('user1', 'has2init') @@ -714,7 +714,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -726,7 +726,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=hasnodigit&token=${token}&username=user1`, + body: `new_password=hasnodigit&token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -737,7 +737,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy` + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy` ); Parse.User.logIn('user1', 'has 1 digit') @@ -900,7 +900,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -912,7 +912,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=xuser12&token=${token}&username=user1`, + body: `new_password=xuser12&token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -923,7 +923,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy` + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy` ); Parse.User.logIn('user1', 'r@nd0m') @@ -991,7 +991,7 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }); expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1003,7 +1003,7 @@ describe('Password Policy: ', () => { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=xuser12&token=${token}&username=user1`, + body: `new_password=xuser12&token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -1051,7 +1051,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1063,7 +1063,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=uuser11&token=${token}&username=user1`, + body: `new_password=uuser11&token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -1074,7 +1074,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' ); Parse.User.logIn('user1', 'uuser11') @@ -1317,7 +1317,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1329,7 +1329,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=uuser11&token=${token}&username=user1`, + body: `new_password=uuser11&token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -1340,7 +1340,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' ); Parse.User.logIn('user1', 'uuser11') @@ -1472,7 +1472,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1484,7 +1484,7 @@ describe('Password Policy: ', () => { return request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=${token}&username=user1`, + body: `new_password=user1&token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -1500,7 +1500,7 @@ describe('Password Policy: ', () => { const token = data[1]; expect(response.status).toEqual(302); expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy` + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy` ); done(); return Promise.resolve(); diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 545662914f..63df9cb42a 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -10,28 +10,6 @@ const request = function (url, callback) { }; describe('public API', () => { - it('should return missing username error on ajax request without username provided', async () => { - await reconfigureServer({ - publicServerURL: 'http://localhost:8378/1', - }); - - try { - await req({ - method: 'POST', - url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=43634643&username=`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest', - }, - followRedirects: false, - }); - } catch (error) { - expect(error.status).not.toBe(302); - expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); - } - }); - it('should return missing token error on ajax request without token provided', async () => { await reconfigureServer({ publicServerURL: 'http://localhost:8378/1', @@ -41,7 +19,7 @@ describe('public API', () => { await req({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=&username=Johnny`, + body: `new_password=user1&token=`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -63,7 +41,7 @@ describe('public API', () => { await req({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=&token=132414&username=Johnny`, + body: `new_password=&token=132414`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', diff --git a/spec/RegexVulnerabilities.spec.js b/spec/RegexVulnerabilities.spec.js index 5d3bdf254d..8a94113b4a 100644 --- a/spec/RegexVulnerabilities.spec.js +++ b/spec/RegexVulnerabilities.spec.js @@ -90,7 +90,7 @@ describe('Regex Vulnerabilities', function () { it('should not work with regex', async function () { expect(this.user.get('emailVerified')).toEqual(false); await request({ - url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token[$regex]=`, + url: `${serverURL}/apps/test/verify_email?token[$regex]=`, method: 'GET', }); await this.user.fetch({ useMasterKey: true }); @@ -112,7 +112,7 @@ describe('Regex Vulnerabilities', function () { }).then(res => res.data); // It should work await request({ - url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${current._email_verify_token}`, + url: `${serverURL}/apps/test/verify_email?token=${current._email_verify_token}`, method: 'GET', }); await this.user.fetch({ useMasterKey: true }); @@ -139,7 +139,7 @@ describe('Regex Vulnerabilities', function () { }); await this.user.fetch({ useMasterKey: true }); const passwordResetResponse = await request({ - url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token[$regex]=`, + url: `${serverURL}/apps/test/request_password_reset?token[$regex]=`, method: 'GET', }); expect(passwordResetResponse.status).toEqual(302); @@ -187,7 +187,7 @@ describe('Regex Vulnerabilities', function () { }).then(res => res.data); const token = current._perishable_token; const passwordResetResponse = await request({ - url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`, + url: `${serverURL}/apps/test/request_password_reset?token=${token}`, method: 'GET', }); expect(passwordResetResponse.status).toEqual(302); diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 6bcc454baf..800031a566 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -19,7 +19,7 @@ describe('UserController', () => { }); emailAdapter.sendVerificationEmail = options => { expect(options.link).toEqual( - 'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser' + 'http://www.example.com/apps/test/verify_email?token=testToken' ); emailAdapter.sendVerificationEmail = () => Promise.resolve(); done(); @@ -41,7 +41,7 @@ describe('UserController', () => { }); emailAdapter.sendVerificationEmail = options => { expect(options.link).toEqual( - 'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser' + 'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken' ); emailAdapter.sendVerificationEmail = () => Promise.resolve(); done(); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 3272f07fc3..4de7b3a36e 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -273,7 +273,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' ); user .fetch({ useMasterKey: true }) @@ -608,7 +608,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' ); user .fetch() @@ -667,12 +667,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => { publicServerURL: 'http://localhost:8378/1', }).then(() => { request({ - url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', + url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf', followRedirects: false, }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=sadfasga&appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' ); done(); }); @@ -712,12 +712,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => { const emailAdapter = { sendVerificationEmail: () => { request({ - url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', + url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid', followRedirects: false, }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=zxcv&appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' ); user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); @@ -757,7 +757,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv%2Bzxcv/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/; expect(response.text.match(re)).not.toBe(null); done(); }); @@ -797,8 +797,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { publicServerURL: 'http://localhost:8378/1', }).then(() => { request({ - url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', + url: 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf', followRedirects: false, }).then(response => { expect(response.status).toEqual(302); @@ -820,7 +819,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -840,7 +839,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' ); Parse.User.logIn('zxcv', 'hello').then( @@ -897,7 +896,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv%2B1/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -917,7 +916,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv%2B1' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' ); done(); }); @@ -956,7 +955,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }); expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1014,7 +1013,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=12345&username=Johnny`, + body: `new_password=user1&token=12345`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 6871add987..948b57c3eb 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -48,14 +48,14 @@ export class UserController extends AdaptableController { } } - verifyEmail(username, token) { + async verifyEmail(token) { if (!this.shouldVerifyEmails) { // Trying to verify email when not enabled // TODO: Better error here. throw undefined; } - const query = { username: username, _email_verify_token: token }; + const query = { _email_verify_token: token }; const updateFields = { emailVerified: true, _email_verify_token: { __op: 'Delete' }, @@ -70,44 +70,37 @@ export class UserController extends AdaptableController { updateFields._email_verify_token_expires_at = { __op: 'Delete' }; } const maintenanceAuth = Auth.maintenance(this.config); - var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', { - username, - }); - return findUserForEmailVerification.execute().then(result => { - if (result.results.length && result.results[0].emailVerified) { - return Promise.resolve(result.results.length[0]); - } else if (result.results.length) { - query.objectId = result.results[0].objectId; - } - return rest.update(this.config, maintenanceAuth, '_User', query, updateFields); - }); + const result = await new RestQuery(this.config, maintenanceAuth, '_User', query).execute(); + if (result.results.length && result.results[0].emailVerified) { + query.objectId = result.results[0].objectId; + } + return await rest.update(this.config, maintenanceAuth, '_User', query, updateFields); } - checkResetTokenValidity(username, token) { - return this.config.database - .find( - '_User', - { - username: username, - _perishable_token: token, - }, - { limit: 1 }, - Auth.maintenance(this.config) - ) - .then(results => { - if (results.length != 1) { - throw 'Failed to reset password: username / email / token is invalid'; - } + async checkResetTokenValidity(token) { + const results = await this.config.database.find( + '_User', + { + _perishable_token: token, + }, + { limit: 1 }, + Auth.maintenance(this.config) + ); + if (results.length !== 1) { + throw 'Failed to reset password: username / email / token is invalid'; + } - if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { - let expiresDate = results[0]._perishable_token_expires_at; - if (expiresDate && expiresDate.__type == 'Date') { - expiresDate = new Date(expiresDate.iso); - } - if (expiresDate < new Date()) throw 'The password reset link has expired'; - } - return results[0]; - }); + if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { + let expiresDate = results[0]._perishable_token_expires_at; + if (expiresDate && expiresDate.__type == 'Date') { + expiresDate = new Date(expiresDate.iso); + } + if (expiresDate < new Date()) { + throw 'The password reset link has expired'; + } + } + + return results[0]; } getUserIfNeeded(user) { @@ -138,9 +131,7 @@ export class UserController extends AdaptableController { const token = encodeURIComponent(user._email_verify_token); // We may need to fetch the user in case of update email this.getUserIfNeeded(user).then(user => { - const username = encodeURIComponent(user.username); - - const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config); + const link = buildEmailLink(this.config.verifyEmailURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -243,9 +234,8 @@ export class UserController extends AdaptableController { user = await this.setPasswordResetToken(email); } const token = encodeURIComponent(user._perishable_token); - const username = encodeURIComponent(user.username); - const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config); + const link = buildEmailLink(this.config.requestResetPasswordURL, token, this.config); const options = { appName: this.config.appName, link: link, @@ -261,21 +251,20 @@ export class UserController extends AdaptableController { return Promise.resolve(user); } - updatePassword(username, token, password) { - return this.checkResetTokenValidity(username, token) - .then(user => updateUserPassword(user, password, this.config)) - .then(user => { - const accountLockoutPolicy = new AccountLockout(user, this.config); - return accountLockoutPolicy.unlockAccount(); - }) - .catch(error => { - if (error && error.message) { - // in case of Parse.Error, fail with the error message only - return Promise.reject(error.message); - } else { - return Promise.reject(error); - } - }); + async updatePassword(token, password) { + try { + const rawUser = await this.checkResetTokenValidity(token); + const user = await updateUserPassword(rawUser, password, this.config); + + const accountLockoutPolicy = new AccountLockout(user, this.config); + return await accountLockoutPolicy.unlockAccount(); + } catch (error) { + if (error && error.message) { + // in case of Parse.Error, fail with the error message only + return Promise.reject(error.message); + } + return Promise.reject(error); + } } defaultVerificationEmail({ link, user, appName }) { @@ -325,17 +314,14 @@ function updateUserPassword(user, password, config) { .then(() => user); } -function buildEmailLink(destination, username, token, config) { - const usernameAndToken = `token=${token}&username=${username}`; - +function buildEmailLink(destination, token, config) { + token = `token=${token}`; if (config.parseFrameURL) { const destinationWithoutHost = destination.replace(config.publicServerURL, ''); - return `${config.parseFrameURL}?link=${encodeURIComponent( - destinationWithoutHost - )}&${usernameAndToken}`; + return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${token}`; } else { - return `${destination}?${usernameAndToken}`; + return `${destination}?${token}`; } } diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 183268a191..f7b9e5574d 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -300,11 +300,8 @@ const load = parseGraphQLSchema => { type: new GraphQLNonNull(GraphQLBoolean), }, }, - mutateAndGetPayload: async ({ username, password, token }, context) => { + mutateAndGetPayload: async ({ password, token }, context) => { const { config } = context; - if (!username) { - throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'you must provide a username'); - } if (!password) { throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'you must provide a password'); } @@ -313,7 +310,7 @@ const load = parseGraphQLSchema => { } const userController = config.userController; - await userController.updatePassword(username, token, password); + await userController.updatePassword(token, password); return { ok: true }; }, }); diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 5d5a1467a7..fbd559b4c0 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -83,30 +83,24 @@ export class PagesRouter extends PromiseRouter { verifyEmail(req) { const config = req.config; - const { username, token: rawToken } = req.query; + const { token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if (!config) { this.invalidRequest(); } - if (!token || !username) { + if (!token) { return this.goToPage(req, pages.emailVerificationLinkInvalid); } const userController = config.userController; - return userController.verifyEmail(username, token).then( + return userController.verifyEmail(token).then( () => { - const params = { - [pageParams.username]: username, - }; - return this.goToPage(req, pages.emailVerificationSuccess, params); + return this.goToPage(req, pages.emailVerificationSuccess); }, () => { - const params = { - [pageParams.username]: username, - }; - return this.goToPage(req, pages.emailVerificationLinkExpired, params); + return this.goToPage(req, pages.emailVerificationLinkExpired); } ); } @@ -154,28 +148,24 @@ export class PagesRouter extends PromiseRouter { this.invalidRequest(); } - const { username, token: rawToken } = req.query; + const { token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; - if (!username || !token) { + if (!token) { return this.goToPage(req, pages.passwordResetLinkInvalid); } - return config.userController.checkResetTokenValidity(username, token).then( + return config.userController.checkResetTokenValidity(token).then( () => { const params = { [pageParams.token]: token, - [pageParams.username]: username, [pageParams.appId]: config.applicationId, [pageParams.appName]: config.appName, }; return this.goToPage(req, pages.passwordReset, params); }, () => { - const params = { - [pageParams.username]: username, - }; - return this.goToPage(req, pages.passwordResetLinkInvalid, params); + return this.goToPage(req, pages.passwordResetLinkInvalid); } ); } @@ -187,17 +177,13 @@ export class PagesRouter extends PromiseRouter { this.invalidRequest(); } - const { username, new_password, token: rawToken } = req.body; + const { new_password, token: rawToken } = req.body; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; - if ((!username || !token || !new_password) && req.xhr === false) { + if ((!token || !new_password) && req.xhr === false) { return this.goToPage(req, pages.passwordResetLinkInvalid); } - if (!username) { - throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'Missing username'); - } - if (!token) { throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing token'); } @@ -207,7 +193,7 @@ export class PagesRouter extends PromiseRouter { } return config.userController - .updatePassword(username, token, new_password) + .updatePassword(token, new_password) .then( () => { return Promise.resolve({ @@ -235,11 +221,8 @@ export class PagesRouter extends PromiseRouter { } const query = result.success - ? { - [pageParams.username]: username, - } + ? {} : { - [pageParams.username]: username, [pageParams.token]: token, [pageParams.appId]: config.applicationId, [pageParams.error]: result.err, diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 5009ee7d22..f1cb7e2ac0 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -11,7 +11,7 @@ const views = path.resolve(__dirname, '../../views'); export class PublicAPIRouter extends PromiseRouter { verifyEmail(req) { - const { username, token: rawToken } = req.query; + const { token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; const appId = req.params.appId; @@ -25,17 +25,16 @@ export class PublicAPIRouter extends PromiseRouter { return this.missingPublicServerURL(); } - if (!token || !username) { + if (!token) { return this.invalidLink(req); } const userController = config.userController; - return userController.verifyEmail(username, token).then( + return userController.verifyEmail(token).then( () => { - const params = qs.stringify({ username }); return Promise.resolve({ status: 302, - location: `${config.verifyEmailSuccessURL}?${params}`, + location: `${config.verifyEmailSuccessURL}`, }); }, () => { @@ -117,19 +116,18 @@ export class PublicAPIRouter extends PromiseRouter { return this.missingPublicServerURL(); } - const { username, token: rawToken } = req.query; + const { token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; - if (!username || !token) { + if (!token) { return this.invalidLink(req); } - return config.userController.checkResetTokenValidity(username, token).then( + return config.userController.checkResetTokenValidity(token).then( () => { const params = qs.stringify({ token, id: config.applicationId, - username, app: config.appName, }); return Promise.resolve({ @@ -154,17 +152,13 @@ export class PublicAPIRouter extends PromiseRouter { return this.missingPublicServerURL(); } - const { username, new_password, token: rawToken } = req.body; + const { new_password, token: rawToken } = req.body; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; - if ((!username || !token || !new_password) && req.xhr === false) { + if ((!token || !new_password) && req.xhr === false) { return this.invalidLink(req); } - if (!username) { - throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'Missing username'); - } - if (!token) { throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing token'); } @@ -174,7 +168,7 @@ export class PublicAPIRouter extends PromiseRouter { } return config.userController - .updatePassword(username, token, new_password) + .updatePassword(token, new_password) .then( () => { return Promise.resolve({ @@ -190,7 +184,6 @@ export class PublicAPIRouter extends PromiseRouter { ) .then(result => { const params = qs.stringify({ - username: username, token: token, id: config.applicationId, error: result.err, @@ -209,9 +202,8 @@ export class PublicAPIRouter extends PromiseRouter { } } - const encodedUsername = encodeURIComponent(username); const location = result.success - ? `${config.passwordResetSuccessURL}?username=${encodedUsername}` + ? `${config.passwordResetSuccessURL}` : `${config.choosePasswordURL}?${params}`; return Promise.resolve({ @@ -230,9 +222,8 @@ export class PublicAPIRouter extends PromiseRouter { invalidVerificationLink(req) { const config = req.config; - if (req.query.username && req.params.appId) { + if (req.params.appId) { const params = qs.stringify({ - username: req.query.username, appId: req.params.appId, }); return Promise.resolve({ From 407825df9902fc258b68c9f5f521ff8c8f485cba Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 30 Mar 2023 13:42:22 +1100 Subject: [PATCH 2/5] tests --- spec/PagesRouter.spec.js | 18 ++++-------------- spec/ParseLiveQuery.spec.js | 2 +- src/Controllers/UserController.js | 2 +- src/Routers/PagesRouter.js | 2 +- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 319d19c1b8..07ac773b7d 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -658,13 +658,11 @@ describe('Pages Router', () => { const appId = linkResponse.headers['x-parse-page-param-appid']; const token = linkResponse.headers['x-parse-page-param-token']; const locale = linkResponse.headers['x-parse-page-param-locale']; - const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; const passwordResetPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(token).toBeDefined(); expect(locale).toBeDefined(); - expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(passwordResetPagePath).toMatch( new RegExp(`\/${exampleLocale}\/${pages.passwordReset.defaultFile}`) @@ -678,7 +676,6 @@ describe('Pages Router', () => { body: { token, locale, - username, new_password: 'newPassword', }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -773,15 +770,13 @@ describe('Pages Router', () => { const appId = linkResponse.headers['x-parse-page-param-appid']; const locale = linkResponse.headers['x-parse-page-param-locale']; - const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(locale).toBe(exampleLocale); - expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`) ); const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; @@ -790,7 +785,7 @@ describe('Pages Router', () => { method: 'POST', body: { locale, - username, + username: 'exampleUsername', }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirects: false, @@ -826,15 +821,13 @@ describe('Pages Router', () => { const appId = linkResponse.headers['x-parse-page-param-appid']; const locale = linkResponse.headers['x-parse-page-param-locale']; - const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(locale).toBe(exampleLocale); - expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`) ); spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => @@ -847,7 +840,7 @@ describe('Pages Router', () => { method: 'POST', body: { locale, - username, + username: 'exampleUsername', }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirects: false, @@ -1132,12 +1125,10 @@ describe('Pages Router', () => { const appId = linkResponse.headers['x-parse-page-param-appid']; const token = linkResponse.headers['x-parse-page-param-token']; - const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; const passwordResetPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(token).toBeDefined(); - expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(passwordResetPagePath).toMatch(new RegExp(`\/${pages.passwordReset.defaultFile}`)); pageResponse.calls.reset(); @@ -1148,7 +1139,6 @@ describe('Pages Router', () => { method: 'POST', body: { token, - username, new_password: 'newPassword', }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index 38259f50d0..f19b98cee0 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -1037,7 +1037,7 @@ describe('ParseLiveQuery', function () { const userController = new UserController(emailAdapter, 'test', { verifyUserEmails: true, }); - userController.verifyEmail(foundUser.username, foundUser._email_verify_token); + userController.verifyEmail(foundUser._email_verify_token); }); }); }); diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 948b57c3eb..f8464a5beb 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -71,7 +71,7 @@ export class UserController extends AdaptableController { } const maintenanceAuth = Auth.maintenance(this.config); const result = await new RestQuery(this.config, maintenanceAuth, '_User', query).execute(); - if (result.results.length && result.results[0].emailVerified) { + if (result.results.length) { query.objectId = result.results[0].objectId; } return await rest.update(this.config, maintenanceAuth, '_User', query, updateFields); diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index fbd559b4c0..f79edb1934 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -100,7 +100,7 @@ export class PagesRouter extends PromiseRouter { return this.goToPage(req, pages.emailVerificationSuccess); }, () => { - return this.goToPage(req, pages.emailVerificationLinkExpired); + return this.goToPage(req, pages.emailVerificationLinkInvalid); } ); } From 0c49a4a3cceddf641b3d395a499896ac9074c0c6 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 30 Mar 2023 16:23:30 +1100 Subject: [PATCH 3/5] feat: allow Pointers in cloud code params --- spec/CloudCode.spec.js | 21 +++++++++++++++++++++ src/Routers/FunctionsRouter.js | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index c02999ad51..e77b1c69a7 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1352,6 +1352,27 @@ describe('Cloud Code', () => { }); }); + it('allow cloud to encode Parse Objects', async () => { + const user = new Parse.User(); + user.setUsername('username'); + user.setPassword('password'); + user.set('deleted', false); + await user.signUp(); + Parse.Cloud.define( + 'deleteAccount', + async req => { + expect(req.params.object instanceof Parse.Object).toBeTrue(); + req.params.object.set('deleted', true); + await req.params.object.save(null, { useMasterKey: true }); + return 'Object deleted'; + }, + { + requireMaster: true, + } + ); + await Parse.Cloud.run('deleteAccount', { object: user.toPointer() }, { useMasterKey: true }); + }); + it('beforeSave should not affect fetched pointers', done => { Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => {}); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index d239908103..84f63a275c 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -18,6 +18,12 @@ function parseObject(obj) { return Object.assign(new Date(obj.iso), obj); } else if (obj && obj.__type == 'File') { return Parse.File.fromJSON(obj); + } else if (obj && obj.__type == 'Pointer') { + return Parse.Object.fromJSON({ + __type: 'Pointer', + className: obj.className, + objectId: obj.objectId, + }); } else if (obj && typeof obj === 'object') { return parseParams(obj); } else { From c38a4a9c9d106a7619b1ca208f09ad23c869f98e Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 30 Mar 2023 16:25:11 +1100 Subject: [PATCH 4/5] Revert "tests" This reverts commit 407825df9902fc258b68c9f5f521ff8c8f485cba. --- spec/PagesRouter.spec.js | 18 ++++++++++++++---- spec/ParseLiveQuery.spec.js | 2 +- src/Controllers/UserController.js | 2 +- src/Routers/PagesRouter.js | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 07ac773b7d..319d19c1b8 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -658,11 +658,13 @@ describe('Pages Router', () => { const appId = linkResponse.headers['x-parse-page-param-appid']; const token = linkResponse.headers['x-parse-page-param-token']; const locale = linkResponse.headers['x-parse-page-param-locale']; + const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; const passwordResetPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(token).toBeDefined(); expect(locale).toBeDefined(); + expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(passwordResetPagePath).toMatch( new RegExp(`\/${exampleLocale}\/${pages.passwordReset.defaultFile}`) @@ -676,6 +678,7 @@ describe('Pages Router', () => { body: { token, locale, + username, new_password: 'newPassword', }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -770,13 +773,15 @@ describe('Pages Router', () => { const appId = linkResponse.headers['x-parse-page-param-appid']; const locale = linkResponse.headers['x-parse-page-param-locale']; + const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(locale).toBe(exampleLocale); + expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`) ); const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; @@ -785,7 +790,7 @@ describe('Pages Router', () => { method: 'POST', body: { locale, - username: 'exampleUsername', + username, }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirects: false, @@ -821,13 +826,15 @@ describe('Pages Router', () => { const appId = linkResponse.headers['x-parse-page-param-appid']; const locale = linkResponse.headers['x-parse-page-param-locale']; + const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(locale).toBe(exampleLocale); + expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`) + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`) ); spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => @@ -840,7 +847,7 @@ describe('Pages Router', () => { method: 'POST', body: { locale, - username: 'exampleUsername', + username, }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirects: false, @@ -1125,10 +1132,12 @@ describe('Pages Router', () => { const appId = linkResponse.headers['x-parse-page-param-appid']; const token = linkResponse.headers['x-parse-page-param-token']; + const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; const passwordResetPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(token).toBeDefined(); + expect(username).toBeDefined(); expect(publicServerUrl).toBeDefined(); expect(passwordResetPagePath).toMatch(new RegExp(`\/${pages.passwordReset.defaultFile}`)); pageResponse.calls.reset(); @@ -1139,6 +1148,7 @@ describe('Pages Router', () => { method: 'POST', body: { token, + username, new_password: 'newPassword', }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index f19b98cee0..38259f50d0 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -1037,7 +1037,7 @@ describe('ParseLiveQuery', function () { const userController = new UserController(emailAdapter, 'test', { verifyUserEmails: true, }); - userController.verifyEmail(foundUser._email_verify_token); + userController.verifyEmail(foundUser.username, foundUser._email_verify_token); }); }); }); diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index f8464a5beb..948b57c3eb 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -71,7 +71,7 @@ export class UserController extends AdaptableController { } const maintenanceAuth = Auth.maintenance(this.config); const result = await new RestQuery(this.config, maintenanceAuth, '_User', query).execute(); - if (result.results.length) { + if (result.results.length && result.results[0].emailVerified) { query.objectId = result.results[0].objectId; } return await rest.update(this.config, maintenanceAuth, '_User', query, updateFields); diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index f79edb1934..fbd559b4c0 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -100,7 +100,7 @@ export class PagesRouter extends PromiseRouter { return this.goToPage(req, pages.emailVerificationSuccess); }, () => { - return this.goToPage(req, pages.emailVerificationLinkInvalid); + return this.goToPage(req, pages.emailVerificationLinkExpired); } ); } From 80ab7b99d875d0c598a67da7af8302e8b4b89df6 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 30 Mar 2023 16:25:18 +1100 Subject: [PATCH 5/5] Revert "fix: remove username from verification emails" This reverts commit 78c67d4c6ea67675c52947749b3ef89623fd0990. --- spec/AccountLockoutPolicy.spec.js | 4 +- spec/EmailVerificationToken.spec.js | 8 +- spec/PagesRouter.spec.js | 30 ++++-- spec/PasswordPolicy.spec.js | 42 ++++----- spec/PublicAPI.spec.js | 26 +++++- spec/RegexVulnerabilities.spec.js | 8 +- spec/UserController.spec.js | 4 +- spec/ValidationAndPasswordsReset.spec.js | 29 +++--- src/Controllers/UserController.js | 114 +++++++++++++---------- src/GraphQL/loaders/usersMutations.js | 7 +- src/Routers/PagesRouter.js | 43 ++++++--- src/Routers/PublicAPIRouter.js | 33 ++++--- 12 files changed, 216 insertions(+), 132 deletions(-) diff --git a/spec/AccountLockoutPolicy.spec.js b/spec/AccountLockoutPolicy.spec.js index da8048adab..43212d0e69 100644 --- a/spec/AccountLockoutPolicy.spec.js +++ b/spec/AccountLockoutPolicy.spec.js @@ -419,7 +419,7 @@ describe('lockout with password reset option', () => { await request({ method: 'POST', url: `${config.publicServerURL}/apps/test/request_password_reset`, - body: `new_password=${newPassword}&token=${token}`, + body: `new_password=${newPassword}&token=${token}&username=${username}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -454,7 +454,7 @@ describe('lockout with password reset option', () => { await request({ method: 'POST', url: `${config.publicServerURL}/apps/test/request_password_reset`, - body: `new_password=${newPassword}&token=${token}`, + body: `new_password=${newPassword}&token=${token}&username=${username}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 2b080c139a..e21a049719 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -39,7 +39,7 @@ describe('Email Verification Token Expiration: ', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' ); done(); }); @@ -133,7 +133,7 @@ describe('Email Verification Token Expiration: ', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' ); done(); }); @@ -392,7 +392,7 @@ describe('Email Verification Token Expiration: ', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' ); done(); }); @@ -445,7 +445,7 @@ describe('Email Verification Token Expiration: ', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' ); done(); }); diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 319d19c1b8..e50144f1fe 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -108,7 +108,7 @@ describe('Pages Router', () => { const res = await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=43634643`, + body: `new_password=user1&token=43634643&username=username`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -124,7 +124,7 @@ describe('Pages Router', () => { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=&token=132414`, + body: `new_password=&token=132414&username=Johnny`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -137,12 +137,30 @@ describe('Pages Router', () => { } }); + it('request_password_reset: responds with AJAX error on missing username', async () => { + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=43634643&username=`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, + }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); + } + }); + it('request_password_reset: responds with AJAX error on missing token', async () => { try { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=`, + body: `new_password=user1&token=&username=Johnny`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -559,7 +577,7 @@ describe('Pages Router', () => { spyOnProperty(Page.prototype, 'defaultFile').and.returnValue(jsonPageFile); const response = await request({ - url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=${exampleLocale}`, + url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=${exampleLocale}`, followRedirects: false, }).catch(e => e); expect(response.status).toEqual(200); @@ -608,7 +626,7 @@ describe('Pages Router', () => { await reconfigureServer(config); const response = await request({ url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT', + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', followRedirects: false, method: 'POST', }); @@ -622,7 +640,7 @@ describe('Pages Router', () => { await reconfigureServer(config); const response = await request({ url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT', + 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT', followRedirects: false, method: 'GET', }); diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index d6f50279f4..4ea6ed2002 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -107,7 +107,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; expect(response.text.match(re)).not.toBe(null); done(); }) @@ -622,7 +622,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -634,7 +634,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=has2init&token=${token}`, + body: `new_password=has2init&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -645,7 +645,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' ); Parse.User.logIn('user1', 'has2init') @@ -714,7 +714,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -726,7 +726,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=hasnodigit&token=${token}`, + body: `new_password=hasnodigit&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -737,7 +737,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy` + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy` ); Parse.User.logIn('user1', 'has 1 digit') @@ -900,7 +900,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -912,7 +912,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=xuser12&token=${token}`, + body: `new_password=xuser12&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -923,7 +923,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy` + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy` ); Parse.User.logIn('user1', 'r@nd0m') @@ -991,7 +991,7 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }); expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1003,7 +1003,7 @@ describe('Password Policy: ', () => { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=xuser12&token=${token}`, + body: `new_password=xuser12&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -1051,7 +1051,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1063,7 +1063,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=uuser11&token=${token}`, + body: `new_password=uuser11&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -1074,7 +1074,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' ); Parse.User.logIn('user1', 'uuser11') @@ -1317,7 +1317,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1329,7 +1329,7 @@ describe('Password Policy: ', () => { request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=uuser11&token=${token}`, + body: `new_password=uuser11&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -1340,7 +1340,7 @@ describe('Password Policy: ', () => { .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' ); Parse.User.logIn('user1', 'uuser11') @@ -1472,7 +1472,7 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1484,7 +1484,7 @@ describe('Password Policy: ', () => { return request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=${token}`, + body: `new_password=user1&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -1500,7 +1500,7 @@ describe('Password Policy: ', () => { const token = data[1]; expect(response.status).toEqual(302); expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy` + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy` ); done(); return Promise.resolve(); diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 63df9cb42a..545662914f 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -10,6 +10,28 @@ const request = function (url, callback) { }; describe('public API', () => { + it('should return missing username error on ajax request without username provided', async () => { + await reconfigureServer({ + publicServerURL: 'http://localhost:8378/1', + }); + + try { + await req({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=43634643&username=`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + followRedirects: false, + }); + } catch (error) { + expect(error.status).not.toBe(302); + expect(error.text).toEqual('{"code":200,"error":"Missing username"}'); + } + }); + it('should return missing token error on ajax request without token provided', async () => { await reconfigureServer({ publicServerURL: 'http://localhost:8378/1', @@ -19,7 +41,7 @@ describe('public API', () => { await req({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=`, + body: `new_password=user1&token=&username=Johnny`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', @@ -41,7 +63,7 @@ describe('public API', () => { await req({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=&token=132414`, + body: `new_password=&token=132414&username=Johnny`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', diff --git a/spec/RegexVulnerabilities.spec.js b/spec/RegexVulnerabilities.spec.js index 8a94113b4a..5d3bdf254d 100644 --- a/spec/RegexVulnerabilities.spec.js +++ b/spec/RegexVulnerabilities.spec.js @@ -90,7 +90,7 @@ describe('Regex Vulnerabilities', function () { it('should not work with regex', async function () { expect(this.user.get('emailVerified')).toEqual(false); await request({ - url: `${serverURL}/apps/test/verify_email?token[$regex]=`, + url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token[$regex]=`, method: 'GET', }); await this.user.fetch({ useMasterKey: true }); @@ -112,7 +112,7 @@ describe('Regex Vulnerabilities', function () { }).then(res => res.data); // It should work await request({ - url: `${serverURL}/apps/test/verify_email?token=${current._email_verify_token}`, + url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${current._email_verify_token}`, method: 'GET', }); await this.user.fetch({ useMasterKey: true }); @@ -139,7 +139,7 @@ describe('Regex Vulnerabilities', function () { }); await this.user.fetch({ useMasterKey: true }); const passwordResetResponse = await request({ - url: `${serverURL}/apps/test/request_password_reset?token[$regex]=`, + url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token[$regex]=`, method: 'GET', }); expect(passwordResetResponse.status).toEqual(302); @@ -187,7 +187,7 @@ describe('Regex Vulnerabilities', function () { }).then(res => res.data); const token = current._perishable_token; const passwordResetResponse = await request({ - url: `${serverURL}/apps/test/request_password_reset?token=${token}`, + url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`, method: 'GET', }); expect(passwordResetResponse.status).toEqual(302); diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 800031a566..6bcc454baf 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -19,7 +19,7 @@ describe('UserController', () => { }); emailAdapter.sendVerificationEmail = options => { expect(options.link).toEqual( - 'http://www.example.com/apps/test/verify_email?token=testToken' + 'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser' ); emailAdapter.sendVerificationEmail = () => Promise.resolve(); done(); @@ -41,7 +41,7 @@ describe('UserController', () => { }); emailAdapter.sendVerificationEmail = options => { expect(options.link).toEqual( - 'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken' + 'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser' ); emailAdapter.sendVerificationEmail = () => Promise.resolve(); done(); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 4de7b3a36e..3272f07fc3 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -273,7 +273,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' ); user .fetch({ useMasterKey: true }) @@ -608,7 +608,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' ); user .fetch() @@ -667,12 +667,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => { publicServerURL: 'http://localhost:8378/1', }).then(() => { request({ - url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf', + url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', followRedirects: false, }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=sadfasga&appId=test' ); done(); }); @@ -712,12 +712,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => { const emailAdapter = { sendVerificationEmail: () => { request({ - url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid', + url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', followRedirects: false, }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=zxcv&appId=test' ); user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); @@ -757,7 +757,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv%2Bzxcv/; expect(response.text.match(re)).not.toBe(null); done(); }); @@ -797,7 +797,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { publicServerURL: 'http://localhost:8378/1', }).then(() => { request({ - url: 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf', + url: + 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', followRedirects: false, }).then(response => { expect(response.status).toEqual(302); @@ -819,7 +820,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -839,7 +840,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv' ); Parse.User.logIn('zxcv', 'hello').then( @@ -896,7 +897,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv%2B1/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -916,7 +917,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }).then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv%2B1' ); done(); }); @@ -955,7 +956,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }); expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1013,7 +1014,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { await request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=user1&token=12345`, + body: `new_password=user1&token=12345&username=Johnny`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 948b57c3eb..6871add987 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -48,14 +48,14 @@ export class UserController extends AdaptableController { } } - async verifyEmail(token) { + verifyEmail(username, token) { if (!this.shouldVerifyEmails) { // Trying to verify email when not enabled // TODO: Better error here. throw undefined; } - const query = { _email_verify_token: token }; + const query = { username: username, _email_verify_token: token }; const updateFields = { emailVerified: true, _email_verify_token: { __op: 'Delete' }, @@ -70,37 +70,44 @@ export class UserController extends AdaptableController { updateFields._email_verify_token_expires_at = { __op: 'Delete' }; } const maintenanceAuth = Auth.maintenance(this.config); - const result = await new RestQuery(this.config, maintenanceAuth, '_User', query).execute(); - if (result.results.length && result.results[0].emailVerified) { - query.objectId = result.results[0].objectId; - } - return await rest.update(this.config, maintenanceAuth, '_User', query, updateFields); + var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', { + username, + }); + return findUserForEmailVerification.execute().then(result => { + if (result.results.length && result.results[0].emailVerified) { + return Promise.resolve(result.results.length[0]); + } else if (result.results.length) { + query.objectId = result.results[0].objectId; + } + return rest.update(this.config, maintenanceAuth, '_User', query, updateFields); + }); } - async checkResetTokenValidity(token) { - const results = await this.config.database.find( - '_User', - { - _perishable_token: token, - }, - { limit: 1 }, - Auth.maintenance(this.config) - ); - if (results.length !== 1) { - throw 'Failed to reset password: username / email / token is invalid'; - } - - if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { - let expiresDate = results[0]._perishable_token_expires_at; - if (expiresDate && expiresDate.__type == 'Date') { - expiresDate = new Date(expiresDate.iso); - } - if (expiresDate < new Date()) { - throw 'The password reset link has expired'; - } - } + checkResetTokenValidity(username, token) { + return this.config.database + .find( + '_User', + { + username: username, + _perishable_token: token, + }, + { limit: 1 }, + Auth.maintenance(this.config) + ) + .then(results => { + if (results.length != 1) { + throw 'Failed to reset password: username / email / token is invalid'; + } - return results[0]; + if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { + let expiresDate = results[0]._perishable_token_expires_at; + if (expiresDate && expiresDate.__type == 'Date') { + expiresDate = new Date(expiresDate.iso); + } + if (expiresDate < new Date()) throw 'The password reset link has expired'; + } + return results[0]; + }); } getUserIfNeeded(user) { @@ -131,7 +138,9 @@ export class UserController extends AdaptableController { const token = encodeURIComponent(user._email_verify_token); // We may need to fetch the user in case of update email this.getUserIfNeeded(user).then(user => { - const link = buildEmailLink(this.config.verifyEmailURL, token, this.config); + const username = encodeURIComponent(user.username); + + const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config); const options = { appName: this.config.appName, link: link, @@ -234,8 +243,9 @@ export class UserController extends AdaptableController { user = await this.setPasswordResetToken(email); } const token = encodeURIComponent(user._perishable_token); + const username = encodeURIComponent(user.username); - const link = buildEmailLink(this.config.requestResetPasswordURL, token, this.config); + const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config); const options = { appName: this.config.appName, link: link, @@ -251,20 +261,21 @@ export class UserController extends AdaptableController { return Promise.resolve(user); } - async updatePassword(token, password) { - try { - const rawUser = await this.checkResetTokenValidity(token); - const user = await updateUserPassword(rawUser, password, this.config); - - const accountLockoutPolicy = new AccountLockout(user, this.config); - return await accountLockoutPolicy.unlockAccount(); - } catch (error) { - if (error && error.message) { - // in case of Parse.Error, fail with the error message only - return Promise.reject(error.message); - } - return Promise.reject(error); - } + updatePassword(username, token, password) { + return this.checkResetTokenValidity(username, token) + .then(user => updateUserPassword(user, password, this.config)) + .then(user => { + const accountLockoutPolicy = new AccountLockout(user, this.config); + return accountLockoutPolicy.unlockAccount(); + }) + .catch(error => { + if (error && error.message) { + // in case of Parse.Error, fail with the error message only + return Promise.reject(error.message); + } else { + return Promise.reject(error); + } + }); } defaultVerificationEmail({ link, user, appName }) { @@ -314,14 +325,17 @@ function updateUserPassword(user, password, config) { .then(() => user); } -function buildEmailLink(destination, token, config) { - token = `token=${token}`; +function buildEmailLink(destination, username, token, config) { + const usernameAndToken = `token=${token}&username=${username}`; + if (config.parseFrameURL) { const destinationWithoutHost = destination.replace(config.publicServerURL, ''); - return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${token}`; + return `${config.parseFrameURL}?link=${encodeURIComponent( + destinationWithoutHost + )}&${usernameAndToken}`; } else { - return `${destination}?${token}`; + return `${destination}?${usernameAndToken}`; } } diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index f7b9e5574d..183268a191 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -300,8 +300,11 @@ const load = parseGraphQLSchema => { type: new GraphQLNonNull(GraphQLBoolean), }, }, - mutateAndGetPayload: async ({ password, token }, context) => { + mutateAndGetPayload: async ({ username, password, token }, context) => { const { config } = context; + if (!username) { + throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'you must provide a username'); + } if (!password) { throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'you must provide a password'); } @@ -310,7 +313,7 @@ const load = parseGraphQLSchema => { } const userController = config.userController; - await userController.updatePassword(token, password); + await userController.updatePassword(username, token, password); return { ok: true }; }, }); diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index fbd559b4c0..5d5a1467a7 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -83,24 +83,30 @@ export class PagesRouter extends PromiseRouter { verifyEmail(req) { const config = req.config; - const { token: rawToken } = req.query; + const { username, token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; if (!config) { this.invalidRequest(); } - if (!token) { + if (!token || !username) { return this.goToPage(req, pages.emailVerificationLinkInvalid); } const userController = config.userController; - return userController.verifyEmail(token).then( + return userController.verifyEmail(username, token).then( () => { - return this.goToPage(req, pages.emailVerificationSuccess); + const params = { + [pageParams.username]: username, + }; + return this.goToPage(req, pages.emailVerificationSuccess, params); }, () => { - return this.goToPage(req, pages.emailVerificationLinkExpired); + const params = { + [pageParams.username]: username, + }; + return this.goToPage(req, pages.emailVerificationLinkExpired, params); } ); } @@ -148,24 +154,28 @@ export class PagesRouter extends PromiseRouter { this.invalidRequest(); } - const { token: rawToken } = req.query; + const { username, token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; - if (!token) { + if (!username || !token) { return this.goToPage(req, pages.passwordResetLinkInvalid); } - return config.userController.checkResetTokenValidity(token).then( + return config.userController.checkResetTokenValidity(username, token).then( () => { const params = { [pageParams.token]: token, + [pageParams.username]: username, [pageParams.appId]: config.applicationId, [pageParams.appName]: config.appName, }; return this.goToPage(req, pages.passwordReset, params); }, () => { - return this.goToPage(req, pages.passwordResetLinkInvalid); + const params = { + [pageParams.username]: username, + }; + return this.goToPage(req, pages.passwordResetLinkInvalid, params); } ); } @@ -177,13 +187,17 @@ export class PagesRouter extends PromiseRouter { this.invalidRequest(); } - const { new_password, token: rawToken } = req.body; + const { username, new_password, token: rawToken } = req.body; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; - if ((!token || !new_password) && req.xhr === false) { + if ((!username || !token || !new_password) && req.xhr === false) { return this.goToPage(req, pages.passwordResetLinkInvalid); } + if (!username) { + throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'Missing username'); + } + if (!token) { throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing token'); } @@ -193,7 +207,7 @@ export class PagesRouter extends PromiseRouter { } return config.userController - .updatePassword(token, new_password) + .updatePassword(username, token, new_password) .then( () => { return Promise.resolve({ @@ -221,8 +235,11 @@ export class PagesRouter extends PromiseRouter { } const query = result.success - ? {} + ? { + [pageParams.username]: username, + } : { + [pageParams.username]: username, [pageParams.token]: token, [pageParams.appId]: config.applicationId, [pageParams.error]: result.err, diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index f1cb7e2ac0..5009ee7d22 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -11,7 +11,7 @@ const views = path.resolve(__dirname, '../../views'); export class PublicAPIRouter extends PromiseRouter { verifyEmail(req) { - const { token: rawToken } = req.query; + const { username, token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; const appId = req.params.appId; @@ -25,16 +25,17 @@ export class PublicAPIRouter extends PromiseRouter { return this.missingPublicServerURL(); } - if (!token) { + if (!token || !username) { return this.invalidLink(req); } const userController = config.userController; - return userController.verifyEmail(token).then( + return userController.verifyEmail(username, token).then( () => { + const params = qs.stringify({ username }); return Promise.resolve({ status: 302, - location: `${config.verifyEmailSuccessURL}`, + location: `${config.verifyEmailSuccessURL}?${params}`, }); }, () => { @@ -116,18 +117,19 @@ export class PublicAPIRouter extends PromiseRouter { return this.missingPublicServerURL(); } - const { token: rawToken } = req.query; + const { username, token: rawToken } = req.query; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; - if (!token) { + if (!username || !token) { return this.invalidLink(req); } - return config.userController.checkResetTokenValidity(token).then( + return config.userController.checkResetTokenValidity(username, token).then( () => { const params = qs.stringify({ token, id: config.applicationId, + username, app: config.appName, }); return Promise.resolve({ @@ -152,13 +154,17 @@ export class PublicAPIRouter extends PromiseRouter { return this.missingPublicServerURL(); } - const { new_password, token: rawToken } = req.body; + const { username, new_password, token: rawToken } = req.body; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; - if ((!token || !new_password) && req.xhr === false) { + if ((!username || !token || !new_password) && req.xhr === false) { return this.invalidLink(req); } + if (!username) { + throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'Missing username'); + } + if (!token) { throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing token'); } @@ -168,7 +174,7 @@ export class PublicAPIRouter extends PromiseRouter { } return config.userController - .updatePassword(token, new_password) + .updatePassword(username, token, new_password) .then( () => { return Promise.resolve({ @@ -184,6 +190,7 @@ export class PublicAPIRouter extends PromiseRouter { ) .then(result => { const params = qs.stringify({ + username: username, token: token, id: config.applicationId, error: result.err, @@ -202,8 +209,9 @@ export class PublicAPIRouter extends PromiseRouter { } } + const encodedUsername = encodeURIComponent(username); const location = result.success - ? `${config.passwordResetSuccessURL}` + ? `${config.passwordResetSuccessURL}?username=${encodedUsername}` : `${config.choosePasswordURL}?${params}`; return Promise.resolve({ @@ -222,8 +230,9 @@ export class PublicAPIRouter extends PromiseRouter { invalidVerificationLink(req) { const config = req.config; - if (req.params.appId) { + if (req.query.username && req.params.appId) { const params = qs.stringify({ + username: req.query.username, appId: req.params.appId, }); return Promise.resolve({