From 78c67d4c6ea67675c52947749b3ef89623fd0990 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 30 Mar 2023 13:18:33 +1100 Subject: [PATCH 01/27] 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 02/27] 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 03/27] 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 3d50b00ca52308bc3b02bd6c9827d4fc10104b85 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 30 Mar 2023 16:24:35 +1100 Subject: [PATCH 04/27] Revert "feat: allow Pointers in cloud code params" This reverts commit 0c49a4a3cceddf641b3d395a499896ac9074c0c6. --- spec/CloudCode.spec.js | 21 --------------------- src/Routers/FunctionsRouter.js | 6 ------ 2 files changed, 27 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index e77b1c69a7..c02999ad51 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1352,27 +1352,6 @@ 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 84f63a275c..d239908103 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -18,12 +18,6 @@ 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 f8144576060179cda742bc59b11a64a775faf5b3 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 16:27:10 +1000 Subject: [PATCH 05/27] wip --- spec/EmailVerificationToken.spec.js | 58 ++++++++++++++++++ spec/ValidationAndPasswordsReset.spec.js | 75 ++++++++++++++++++++++++ src/Controllers/UserController.js | 10 ++-- src/Routers/PagesRouter.js | 11 +++- src/Routers/PublicAPIRouter.js | 21 +++++-- src/Routers/UsersRouter.js | 14 ++++- 6 files changed, 175 insertions(+), 14 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 3066ac2e74..c47274f4e5 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -288,6 +288,64 @@ describe('Email Verification Token Expiration: ', () => { }); }); + it('can resend email using an expired token', async () => { + const user = new Parse.User(); + const emailAdapter = { + sendVerificationEmail: () => {}, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + }); + user.setUsername('test'); + user.setPassword('password'); + user.set('email', 'user@example.com'); + await user.signUp(); + + await Parse.Server.database.update( + '_User', + { objectId: user.id }, + { + _email_verify_token_expires_at: Parse._encode(new Date('2000')), + } + ); + + const obj = await Parse.Server.database.find( + '_User', + { objectId: user.id }, + {}, + Auth.maintenance(Parse.Server) + ); + const token = obj[0]._email_verify_token; + + const res = await request({ + url: `http://localhost:8378/1/apps/test/verify_email?token=${token}`, + method: 'GET', + }); + expect(res.text).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=${token}` + ); + + const formUrl = `http://localhost:8378/1/apps/test/resend_verification_email`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + expiredToken: token, + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.text).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/link_send_success.html` + ); + }); + it('can conditionally send emails', async () => { let sendEmailOptions; const emailAdapter = { diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 1ade937037..65ccd35574 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -3,6 +3,7 @@ const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions'); const request = require('../lib/request'); const Config = require('../lib/Config'); +const Auth = require('../lib/Auth'); describe('Custom Pages, Email Verification, Password Reset', () => { it('should set the custom pages', done => { @@ -1083,6 +1084,80 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); + fit('can resend email using an expired reset password token', async () => { + const user = new Parse.User(); + const emailAdapter = { + sendVerificationEmail: () => {}, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + passwordPolicy: { + resetTokenValidityDuration: 5 * 60, // 5 minutes + }, + silent: false, + }); + user.setUsername('test'); + user.setPassword('password'); + user.set('email', 'user@example.com'); + await user.signUp(); + await Parse.User.requestPasswordReset('user@example.com'); + + await Parse.Server.database.update( + '_User', + { objectId: user.id }, + { + _perishable_token_expires_at: Parse._encode(new Date('2000')), + } + ); + + let obj = await Parse.Server.database.find( + '_User', + { objectId: user.id }, + {}, + Auth.maintenance(Parse.Server) + ); + const token = obj[0]._perishable_token; + const res = await request({ + url: `http://localhost:8378/1/apps/test/request_password_reset`, + method: 'POST', + body: { + token, + new_password: 'newpassword', + }, + }); + expect(res.text).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?id=test&error=The%20password%20reset%20link%20has%20expired&app=emailVerifyToken&expiredToken=${token}` + ); + + await request({ + url: `http://localhost:8378/1/requestPasswordReset`, + method: 'POST', + body: { + expiredToken: token, + }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); + + obj = await Parse.Server.database.find( + '_User', + { objectId: user.id }, + {}, + Auth.maintenance(Parse.Server) + ); + + expect(obj._perishable_token).not.toBe(token); + }); + it('should throw on an invalid reset password', async () => { await reconfigureServer({ appName: 'coolapp', diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 6b1ee4bf4a..d6b604ba3a 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -127,6 +127,10 @@ export class UserController extends AdaptableController { if (user.email) { where.email = user.email; } + if (user._email_verify_token) { + where._email_verify_token = user._email_verify_token; + where._email_verify_token_expires_at = { $lt: Parse._encode(new Date()) }; + } var query = new RestQuery(this.config, Auth.master(this.config), '_User', where); return query.execute().then(function (result) { @@ -157,8 +161,6 @@ export class UserController extends AdaptableController { if (!shouldSendEmail) { return; } - const username = encodeURIComponent(user.username); - const link = buildEmailLink(this.config.verifyEmailURL, token, this.config); const options = { appName: this.config.appName, @@ -199,8 +201,8 @@ export class UserController extends AdaptableController { return this.config.database.update('_User', { username: user.username }, user); } - async resendVerificationEmail(username, req) { - const aUser = await this.getUserIfNeeded({ username: username }); + async resendVerificationEmail(username, req, expiredToken) { + const aUser = await this.getUserIfNeeded({ username, _email_verify_token: expiredToken }); if (!aUser || aUser.emailVerified) { throw undefined; } diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 3759078500..4cdbf7c1d3 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -45,6 +45,7 @@ const pages = Object.freeze({ const pageParams = Object.freeze({ appName: 'appName', appId: 'appId', + expiredToken: 'expiredToken', token: 'token', username: 'username', error: 'error', @@ -108,18 +109,19 @@ export class PagesRouter extends PromiseRouter { resendVerificationEmail(req) { const config = req.config; const username = req.body.username; + const expiredToken = req.body.expiredToken; if (!config) { this.invalidRequest(); } - if (!username) { + if (!username && !expiredToken) { return this.goToPage(req, pages.emailVerificationLinkInvalid); } const userController = config.userController; - return userController.resendVerificationEmail(username, req).then( + return userController.resendVerificationEmail(username, req, expiredToken).then( () => { return this.goToPage(req, pages.emailVerificationSendSuccess); }, @@ -228,6 +230,11 @@ export class PagesRouter extends PromiseRouter { [pageParams.error]: result.err, [pageParams.appName]: config.appName, }; + + if (result?.err === 'The password reset link has expired') { + delete query[pageParams.token]; + query[pageParams.expiredToken] = token; + } const page = result.success ? pages.passwordResetSuccess : pages.passwordReset; return this.goToPage(req, page, query, false); diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index d8b5599a29..a9c8872fc2 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -38,7 +38,7 @@ export class PublicAPIRouter extends PromiseRouter { }); }, () => { - return this.invalidVerificationLink(req); + return this.invalidVerificationLink(req, token); } ); } @@ -56,13 +56,15 @@ export class PublicAPIRouter extends PromiseRouter { return this.missingPublicServerURL(); } - if (!username) { + const expiredToken = req.body.expiredToken; + + if (!username && !expiredToken) { return this.invalidLink(req); } const userController = config.userController; - return userController.resendVerificationEmail(username, req).then( + return userController.resendVerificationEmail(username, req, expiredToken).then( () => { return Promise.resolve({ status: 302, @@ -183,12 +185,18 @@ export class PublicAPIRouter extends PromiseRouter { } ) .then(result => { - const params = qs.stringify({ + const queryString = { token: token, id: config.applicationId, error: result.err, app: config.appName, - }); + }; + + if (result?.err === 'The password reset link has expired') { + delete queryString.token; + queryString.expiredToken = token; + } + const params = qs.stringify(queryString); if (req.xhr) { if (result.success) { @@ -220,11 +228,12 @@ export class PublicAPIRouter extends PromiseRouter { }); } - invalidVerificationLink(req) { + invalidVerificationLink(req, expiredToken) { const config = req.config; if (req.params.appId) { const params = qs.stringify({ appId: req.params.appId, + expiredToken, }); return Promise.resolve({ status: 302, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 14131cf5f1..aca0f01d1d 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -423,10 +423,20 @@ export class UsersRouter extends ClassesRouter { async handleResetRequest(req) { this._throwOnBadEmailConfig(req); - const { email } = req.body; - if (!email) { + let email = req.body.email; + const expiredToken = req.body.expiredToken; + if (!email && !expiredToken) { throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email'); } + if (expiredToken) { + const results = await req.config.database.find('_User', { + _perishable_token: expiredToken, + _perishable_token_expires_at: { $lt: Parse._encode(new Date()) }, + }); + if (results && results[0] && results[0].email) { + email = results[0].email; + } + } if (typeof email !== 'string') { throw new Parse.Error( Parse.Error.INVALID_EMAIL_ADDRESS, From b960a1a5171888392b975f67c531481226367414 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 25 Jul 2023 18:12:52 +1000 Subject: [PATCH 06/27] Update ValidationAndPasswordsReset.spec.js --- spec/ValidationAndPasswordsReset.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 65ccd35574..632bf17d24 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -1084,7 +1084,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); - fit('can resend email using an expired reset password token', async () => { + it('can resend email using an expired reset password token', async () => { const user = new Parse.User(); const emailAdapter = { sendVerificationEmail: () => {}, From 9d4a028abf7ea45ef615dcc308955c9f9b8b52de Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Jan 2025 21:22:03 +1100 Subject: [PATCH 07/27] Update UserController.spec.js --- spec/UserController.spec.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index e1da9b380c..3520dd1b34 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -17,13 +17,12 @@ describe('UserController', () => { }); let emailOptions; - emailAdapter.sendVerificationEmail = options => { - expect(options.link).toEqual( - 'http://www.example.com/apps/test/verify_email?token=testToken' - ); - emailAdapter.sendVerificationEmail = () => Promise.resolve(); - done(); - }; + const promise = new Promise((resolve) => { + emailAdapter.sendVerificationEmail = options => { + emailOptions = options; + resolve(); + }; + }); const username = 'verificationUser'; const user = new Parse.User(); @@ -38,6 +37,8 @@ describe('UserController', () => { const rawToken = rawUser[0]._email_verify_token; expect(rawToken).toBeDefined(); expect(rawUsername).toBe(username); + + await promise; expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}&username=${username}`); }); }); @@ -55,13 +56,12 @@ describe('UserController', () => { }); let emailOptions; - emailAdapter.sendVerificationEmail = options => { - expect(options.link).toEqual( - 'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken' - ); - emailAdapter.sendVerificationEmail = () => Promise.resolve(); - done(); - }; + const promise = new Promise((resolve) => { + emailAdapter.sendVerificationEmail = options => { + emailOptions = options; + resolve(); + }; + }); const username = 'verificationUser'; const user = new Parse.User(); @@ -76,6 +76,8 @@ describe('UserController', () => { const rawToken = rawUser[0]._email_verify_token; expect(rawToken).toBeDefined(); expect(rawUsername).toBe(username); + + await promise; expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}&username=${username}`); }); }); From 72528936c55571f7711c17e514a7f2940dedd363 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Jan 2025 22:23:47 +1100 Subject: [PATCH 08/27] fix failing tests --- spec/EmailVerificationToken.spec.js | 2 +- src/Controllers/UserController.js | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index f4fa3ee9b6..0671ea3d02 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -350,7 +350,7 @@ describe('Email Verification Token Expiration: ', () => { ); }); - it('can conditionally send emails', async () => { + it_id('9365c53c-b8b4-41f7-a3c1-77882f76a89c')(it)('can conditionally send emails', async () => { let sendEmailOptions; const emailAdapter = { sendVerificationEmail: options => { diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 327de235db..53ccccb0f5 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -82,7 +82,15 @@ 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(); + const restQuery = await RestQuery({ + method: RestQuery.Method.get, + config: this.config, + auth: maintenanceAuth, + className: '_User', + restWhere: query, + }); + + const result = await restQuery.execute(); if (result.results.length) { query.objectId = result.results[0].objectId; } @@ -125,7 +133,7 @@ export class UserController extends AdaptableController { } if (user._email_verify_token) { where._email_verify_token = user._email_verify_token; - where._email_verify_token_expires_at = { $lt: Parse._encode(new Date()) }; + where._email_verify_token_expires_at = { $gt: Parse._encode(new Date()) }; } var query = await RestQuery({ From f9b54dd99a2ab47f6ed3cb82f36bd3d7a65f5a11 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Jan 2025 22:38:01 +1100 Subject: [PATCH 09/27] Update UserController.js --- src/Controllers/UserController.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 53ccccb0f5..0358f4fc9a 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -133,7 +133,6 @@ export class UserController extends AdaptableController { } if (user._email_verify_token) { where._email_verify_token = user._email_verify_token; - where._email_verify_token_expires_at = { $gt: Parse._encode(new Date()) }; } var query = await RestQuery({ From 4a72e2e6df7a3952c7e6b0cd47e06ae59b795cc4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Jan 2025 22:58:34 +1100 Subject: [PATCH 10/27] fix tests --- spec/EmailVerificationToken.spec.js | 6 +++--- spec/ValidationAndPasswordsReset.spec.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 0671ea3d02..0858b396fc 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -40,7 +40,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?appId=test&expiredToken=${sendEmailOptions.link.split('token=')[1]}` ); done(); }); @@ -673,7 +673,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?appId=test&expiredToken=${sendEmailOptions.link.split('token=')[1]}` ); done(); }); @@ -726,7 +726,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?appId=test&expiredToken=${sendEmailOptions.link.split('token=')[1]}` ); done(); }); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 1951c0811d..6386f84c86 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -335,7 +335,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); 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 = await new Parse.Query(Parse.User).first({ useMasterKey: true }); expect(user.get('emailVerified')).toEqual(true); From e110732396a88312fc1d6ed1ed5903b7530bdcba Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Jan 2025 23:06:16 +1100 Subject: [PATCH 11/27] Update ValidationAndPasswordsReset.spec.js --- spec/ValidationAndPasswordsReset.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 6386f84c86..b0c5fca4e7 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -740,7 +740,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/invalid_verification_link.html?appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=asdfasdf' ); done(); }); @@ -785,7 +785,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/invalid_verification_link.html?appId=test' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=invalid' ); user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); From 5beb30f7db1154073b99812dc4a79c1b26146671 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 13:26:22 +1100 Subject: [PATCH 12/27] Update ValidationAndPasswordsReset.spec.js --- spec/ValidationAndPasswordsReset.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index b0c5fca4e7..9546e49d6f 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -785,7 +785,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/invalid_verification_link.html?appId=test&token=invalid' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=invalid' ); user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); From 205d59edfaade953242ebac7fec03590807c2227 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 13:49:38 +1100 Subject: [PATCH 13/27] Update UserController.spec.js --- spec/UserController.spec.js | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 3520dd1b34..bc8683410f 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -17,12 +17,9 @@ describe('UserController', () => { }); let emailOptions; - const promise = new Promise((resolve) => { - emailAdapter.sendVerificationEmail = options => { - emailOptions = options; - resolve(); - }; - }); + emailAdapter.sendVerificationEmail = options => { + emailOptions = options; + }; const username = 'verificationUser'; const user = new Parse.User(); @@ -38,8 +35,7 @@ describe('UserController', () => { expect(rawToken).toBeDefined(); expect(rawUsername).toBe(username); - await promise; - expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}&username=${username}`); + expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}}`); }); }); @@ -56,12 +52,9 @@ describe('UserController', () => { }); let emailOptions; - const promise = new Promise((resolve) => { - emailAdapter.sendVerificationEmail = options => { - emailOptions = options; - resolve(); - }; - }); + emailAdapter.sendVerificationEmail = options => { + emailOptions = options; + }; const username = 'verificationUser'; const user = new Parse.User(); @@ -77,8 +70,7 @@ describe('UserController', () => { expect(rawToken).toBeDefined(); expect(rawUsername).toBe(username); - await promise; - expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}&username=${username}`); + expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}`); }); }); }); From db5409a60d1c8e87effe37ee15f9f547b6b69eea Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 15:12:34 +1100 Subject: [PATCH 14/27] fix failing tests --- spec/UserController.spec.js | 2 +- spec/helper.js | 94 ++++++++++++++--------------- spec/support/CurrentSpecReporter.js | 1 + 3 files changed, 46 insertions(+), 51 deletions(-) diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index bc8683410f..31d5051960 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -35,7 +35,7 @@ describe('UserController', () => { expect(rawToken).toBeDefined(); expect(rawUsername).toBe(username); - expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}}`); + expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}`); }); }); diff --git a/spec/helper.js b/spec/helper.js index 7093cfcc4c..2d98526434 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -227,65 +227,59 @@ beforeAll(async () => { Parse.serverURL = 'http://localhost:' + port + '/1'; }); -afterEach(function (done) { - const afterLogOut = async () => { - // Jasmine process uses one connection +global.afterEachFn = async () => { + try { + Parse.Cloud._removeAllHooks(); + Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(); + defaults.protectedFields = { _User: { '*': ['email'] } }; + + const allSchemas = await databaseAdapter.getAllClasses(); + + allSchemas.forEach(schema => { + const className = schema.className; + expect(className).toEqual({ + asymmetricMatch: className => { + if (!className.startsWith('_')) { + return true; + } + return [ + '_User', + '_Installation', + '_Role', + '_Session', + '_Product', + '_Audience', + '_Idempotency', + ].includes(className); + }, + }); + }); + + await Parse.User.logOut(); + + // Connection close events are not immediate on node 10+, so wait a bit + await new Promise(resolve => setTimeout(resolve, 0)); + + // After logout operations if (Object.keys(openConnections).length > 1) { - console.warn(`There were ${Object.keys(openConnections).length} open connections to the server left after the test finished`); + console.warn( + `There were ${Object.keys(openConnections).length} open connections to the server left after the test finished` + ); } + await TestUtils.destroyAllDataPermanently(true); SchemaCache.clear(); + if (didChangeConfiguration) { await reconfigureServer(); } else { await databaseAdapter.performInitialization({ VolatileClassesSchemas }); } - done(); - }; - Parse.Cloud._removeAllHooks(); - Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(); - defaults.protectedFields = { _User: { '*': ['email'] } }; - databaseAdapter - .getAllClasses() - .then(allSchemas => { - allSchemas.forEach(schema => { - const className = schema.className; - expect(className).toEqual({ - asymmetricMatch: className => { - if (!className.startsWith('_')) { - return true; - } else { - // Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will - // break it. - return ( - [ - '_User', - '_Installation', - '_Role', - '_Session', - '_Product', - '_Audience', - '_Idempotency', - ].indexOf(className) >= 0 - ); - } - }, - }); - }); - }) - .then(() => Parse.User.logOut()) - .then( - () => {}, - () => {} - ) // swallow errors - .then(() => { - // Connection close events are not immediate on node 10+... wait a bit - return new Promise(resolve => { - setTimeout(resolve, 0); - }); - }) - .then(afterLogOut); -}); + } catch (error) { + // Swallow errors + } +} +afterEach(afterEachFn); afterAll(() => { global.displayTestStats(); diff --git a/spec/support/CurrentSpecReporter.js b/spec/support/CurrentSpecReporter.js index 0b3aab4fba..b0b198fc50 100755 --- a/spec/support/CurrentSpecReporter.js +++ b/spec/support/CurrentSpecReporter.js @@ -108,6 +108,7 @@ global.retryFlakyTests = function() { } if (isFlaky) { retryMap[spec.result.fullName] = (retryMap[spec.result.fullName] || 0) + 1; + await global.afterEachFn(); } } if (exceptionCaught) { From 170f83ab7863aafba61813ea6a46d3385d532fb9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 15:26:24 +1100 Subject: [PATCH 15/27] add logging --- spec/helper.js | 6 +++--- spec/support/CurrentSpecReporter.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 2d98526434..08c8e7dac9 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -255,7 +255,7 @@ global.afterEachFn = async () => { }); }); - await Parse.User.logOut(); + await Parse.User.logOut().catch(() => {}); // Connection close events are not immediate on node 10+, so wait a bit await new Promise(resolve => setTimeout(resolve, 0)); @@ -276,10 +276,10 @@ global.afterEachFn = async () => { await databaseAdapter.performInitialization({ VolatileClassesSchemas }); } } catch (error) { - // Swallow errors + console.error('An error occured in the afterEach function', error); } } -afterEach(afterEachFn); +afterEach(global.afterEachFn); afterAll(() => { global.displayTestStats(); diff --git a/spec/support/CurrentSpecReporter.js b/spec/support/CurrentSpecReporter.js index b0b198fc50..6fe21aba9b 100755 --- a/spec/support/CurrentSpecReporter.js +++ b/spec/support/CurrentSpecReporter.js @@ -108,8 +108,8 @@ global.retryFlakyTests = function() { } if (isFlaky) { retryMap[spec.result.fullName] = (retryMap[spec.result.fullName] || 0) + 1; - await global.afterEachFn(); } + await global.afterEachFn(); } if (exceptionCaught) { throw exceptionCaught; From 6ff9e6b7e3b56e19c0dc2f93c9946874b41ee0c9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 15:35:50 +1100 Subject: [PATCH 16/27] Update CurrentSpecReporter.js --- spec/support/CurrentSpecReporter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/CurrentSpecReporter.js b/spec/support/CurrentSpecReporter.js index 6fe21aba9b..b0b198fc50 100755 --- a/spec/support/CurrentSpecReporter.js +++ b/spec/support/CurrentSpecReporter.js @@ -108,8 +108,8 @@ global.retryFlakyTests = function() { } if (isFlaky) { retryMap[spec.result.fullName] = (retryMap[spec.result.fullName] || 0) + 1; + await global.afterEachFn(); } - await global.afterEachFn(); } if (exceptionCaught) { throw exceptionCaught; From e1ed76b3865ac7fab2561a948ddb387f8d89630a Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 15:52:02 +1100 Subject: [PATCH 17/27] revert resolve --- spec/helper.js | 93 ++++++++++++++++------------- spec/support/CurrentSpecReporter.js | 2 +- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 08c8e7dac9..f9c2c043ac 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -227,59 +227,66 @@ beforeAll(async () => { Parse.serverURL = 'http://localhost:' + port + '/1'; }); -global.afterEachFn = async () => { - try { - Parse.Cloud._removeAllHooks(); - Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(); - defaults.protectedFields = { _User: { '*': ['email'] } }; - - const allSchemas = await databaseAdapter.getAllClasses(); - - allSchemas.forEach(schema => { - const className = schema.className; - expect(className).toEqual({ - asymmetricMatch: className => { - if (!className.startsWith('_')) { - return true; - } - return [ - '_User', - '_Installation', - '_Role', - '_Session', - '_Product', - '_Audience', - '_Idempotency', - ].includes(className); - }, - }); - }); - - await Parse.User.logOut().catch(() => {}); - - // Connection close events are not immediate on node 10+, so wait a bit - await new Promise(resolve => setTimeout(resolve, 0)); - - // After logout operations +global.afterEachFn = function (done) { + const afterLogOut = async () => { + // Jasmine process uses one connection if (Object.keys(openConnections).length > 1) { - console.warn( - `There were ${Object.keys(openConnections).length} open connections to the server left after the test finished` - ); + console.warn(`There were ${Object.keys(openConnections).length} open connections to the server left after the test finished`); } - await TestUtils.destroyAllDataPermanently(true); SchemaCache.clear(); - if (didChangeConfiguration) { await reconfigureServer(); } else { await databaseAdapter.performInitialization({ VolatileClassesSchemas }); } - } catch (error) { - console.error('An error occured in the afterEach function', error); - } + done(); + }; + Parse.Cloud._removeAllHooks(); + Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(); + defaults.protectedFields = { _User: { '*': ['email'] } }; + databaseAdapter + .getAllClasses() + .then(allSchemas => { + allSchemas.forEach(schema => { + const className = schema.className; + expect(className).toEqual({ + asymmetricMatch: className => { + if (!className.startsWith('_')) { + return true; + } else { + // Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will + // break it. + return ( + [ + '_User', + '_Installation', + '_Role', + '_Session', + '_Product', + '_Audience', + '_Idempotency', + ].indexOf(className) >= 0 + ); + } + }, + }); + }); + }) + .then(() => Parse.User.logOut()) + .then( + () => {}, + () => {} + ) // swallow errors + .then(() => { + // Connection close events are not immediate on node 10+... wait a bit + return new Promise(resolve => { + setTimeout(resolve, 0); + }); + }) + .then(afterLogOut); } -afterEach(global.afterEachFn); +afterEach(afterEachFn); afterAll(() => { global.displayTestStats(); diff --git a/spec/support/CurrentSpecReporter.js b/spec/support/CurrentSpecReporter.js index b0b198fc50..23a8d43236 100755 --- a/spec/support/CurrentSpecReporter.js +++ b/spec/support/CurrentSpecReporter.js @@ -108,7 +108,7 @@ global.retryFlakyTests = function() { } if (isFlaky) { retryMap[spec.result.fullName] = (retryMap[spec.result.fullName] || 0) + 1; - await global.afterEachFn(); + await new Promise(resolve => global.afterEachFn(resolve)); } } if (exceptionCaught) { From 0d8a4a2c8d2d209abdcb6df64cde3e7c582eb398 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Jan 2025 16:25:55 +1100 Subject: [PATCH 18/27] add catch --- spec/helper.js | 101 +++++++++++++--------------- spec/support/CurrentSpecReporter.js | 2 +- 2 files changed, 46 insertions(+), 57 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index f9c2c043ac..bc20ebcf5e 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -227,66 +227,55 @@ beforeAll(async () => { Parse.serverURL = 'http://localhost:' + port + '/1'; }); -global.afterEachFn = function (done) { - const afterLogOut = async () => { - // Jasmine process uses one connection - if (Object.keys(openConnections).length > 1) { - console.warn(`There were ${Object.keys(openConnections).length} open connections to the server left after the test finished`); - } - await TestUtils.destroyAllDataPermanently(true); - SchemaCache.clear(); - if (didChangeConfiguration) { - await reconfigureServer(); - } else { - await databaseAdapter.performInitialization({ VolatileClassesSchemas }); - } - done(); - }; +global.afterEachFn = async () => { Parse.Cloud._removeAllHooks(); Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(); defaults.protectedFields = { _User: { '*': ['email'] } }; - databaseAdapter - .getAllClasses() - .then(allSchemas => { - allSchemas.forEach(schema => { - const className = schema.className; - expect(className).toEqual({ - asymmetricMatch: className => { - if (!className.startsWith('_')) { - return true; - } else { - // Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will - // break it. - return ( - [ - '_User', - '_Installation', - '_Role', - '_Session', - '_Product', - '_Audience', - '_Idempotency', - ].indexOf(className) >= 0 - ); - } - }, - }); - }); - }) - .then(() => Parse.User.logOut()) - .then( - () => {}, - () => {} - ) // swallow errors - .then(() => { - // Connection close events are not immediate on node 10+... wait a bit - return new Promise(resolve => { - setTimeout(resolve, 0); - }); - }) - .then(afterLogOut); + + const allSchemas = await databaseAdapter.getAllClasses().catch(() => []); + + allSchemas.forEach(schema => { + const className = schema.className; + expect(className).toEqual({ + asymmetricMatch: className => { + if (!className.startsWith('_')) { + return true; + } + return [ + '_User', + '_Installation', + '_Role', + '_Session', + '_Product', + '_Audience', + '_Idempotency', + ].includes(className); + }, + }); + }); + + await Parse.User.logOut().catch(() => {}); + + // Connection close events are not immediate on node 10+, so wait a bit + await new Promise(resolve => setTimeout(resolve, 0)); + + // After logout operations + if (Object.keys(openConnections).length > 1) { + console.warn( + `There were ${Object.keys(openConnections).length} open connections to the server left after the test finished` + ); + } + + await TestUtils.destroyAllDataPermanently(true); + SchemaCache.clear(); + + if (didChangeConfiguration) { + await reconfigureServer(); + } else { + await databaseAdapter.performInitialization({ VolatileClassesSchemas }); + } } -afterEach(afterEachFn); +afterEach(global.afterEachFn); afterAll(() => { global.displayTestStats(); diff --git a/spec/support/CurrentSpecReporter.js b/spec/support/CurrentSpecReporter.js index 23a8d43236..b0b198fc50 100755 --- a/spec/support/CurrentSpecReporter.js +++ b/spec/support/CurrentSpecReporter.js @@ -108,7 +108,7 @@ global.retryFlakyTests = function() { } if (isFlaky) { retryMap[spec.result.fullName] = (retryMap[spec.result.fullName] || 0) + 1; - await new Promise(resolve => global.afterEachFn(resolve)); + await global.afterEachFn(); } } if (exceptionCaught) { From 3295166c8e9b1a10436cbafd9fb10811e22adb55 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 30 Jan 2025 19:06:02 +1100 Subject: [PATCH 19/27] Create 8.0.0.md --- 8.0.0.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 8.0.0.md diff --git a/8.0.0.md b/8.0.0.md new file mode 100644 index 0000000000..a346669310 --- /dev/null +++ b/8.0.0.md @@ -0,0 +1,30 @@ +# Parse Server 8 Migration Guide + +This document highlights specific changes that require a longer explanation. For a full list of changes in Parse Server 8, please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md). + +--- + +- [Invalid Link Page Changes](#invalid-link-page-changes) + +--- + +## Invalid Link Page Changes + +In Parse Server 8, the invalid link page will no longer provide the `username` URL parameter. Instead, the URL parameter will now be `expiredToken`. This change affects how expired verification emails are handled. + +### Regenerating a Verification Email Request + +To regenerate a verification email request, send a `POST` request to the following endpoint: + +``` +HTTP +Method: POST +URL: {{server url}}/resend_verification_email + +Headers: +Content-Type: application/x-www-form-urlencoded + +Body: +expiredToken={token} +``` + From 4808c8a24d82a5cd1163256c709fb12590ef4dbc Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 3 Feb 2025 20:43:58 +1100 Subject: [PATCH 20/27] fix pages router --- public/de-AT/email_verification_link_expired.html | 2 +- public/de/email_verification_link_expired.html | 2 +- public/email_verification_link_expired.html | 2 +- public_html/invalid_verification_link.html | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/de-AT/email_verification_link_expired.html b/public/de-AT/email_verification_link_expired.html index bea8d949fb..46833cbe2c 100644 --- a/public/de-AT/email_verification_link_expired.html +++ b/public/de-AT/email_verification_link_expired.html @@ -15,7 +15,7 @@

{{appName}}

Expired verification link!

- +
diff --git a/public/de/email_verification_link_expired.html b/public/de/email_verification_link_expired.html index bea8d949fb..46833cbe2c 100644 --- a/public/de/email_verification_link_expired.html +++ b/public/de/email_verification_link_expired.html @@ -15,7 +15,7 @@

{{appName}}

Expired verification link!

- +
diff --git a/public/email_verification_link_expired.html b/public/email_verification_link_expired.html index bea8d949fb..46833cbe2c 100644 --- a/public/email_verification_link_expired.html +++ b/public/email_verification_link_expired.html @@ -15,7 +15,7 @@

{{appName}}

Expired verification link!

- +
diff --git a/public_html/invalid_verification_link.html b/public_html/invalid_verification_link.html index fe6914fc82..b61dfdf655 100644 --- a/public_html/invalid_verification_link.html +++ b/public_html/invalid_verification_link.html @@ -47,8 +47,8 @@ window.onload = addDataToForm; function addDataToForm() { - var username = getUrlParameter("username"); - document.getElementById("usernameField").value = username; + const expiredToken = getUrlParameter("expiredToken"); + document.getElementById("expiredToken").value = expiredToken; var appId = getUrlParameter("appId"); document.getElementById("resendForm").action = '/apps/' + appId + '/resend_verification_email' @@ -60,7 +60,7 @@

Invalid Verification Link

- +
From d60bbd2b946561f611c637b58bf7c4b1a14960e3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 12 Feb 2025 22:10:03 +1100 Subject: [PATCH 21/27] review feedback --- 8.0.0.md | 18 +++++------------- .../de-AT/email_verification_link_expired.html | 2 +- public/de/email_verification_link_expired.html | 2 +- public/email_verification_link_expired.html | 2 +- public_html/invalid_verification_link.html | 6 +++--- spec/EmailVerificationToken.spec.js | 16 +++++++++++----- spec/ValidationAndPasswordsReset.spec.js | 8 ++++---- src/Controllers/UserController.js | 4 ++-- src/Routers/PagesRouter.js | 9 ++++----- src/Routers/PublicAPIRouter.js | 12 ++++++------ src/Routers/UsersRouter.js | 8 ++++---- 11 files changed, 42 insertions(+), 45 deletions(-) diff --git a/8.0.0.md b/8.0.0.md index a346669310..3bb9633a54 100644 --- a/8.0.0.md +++ b/8.0.0.md @@ -1,20 +1,12 @@ -# Parse Server 8 Migration Guide - -This document highlights specific changes that require a longer explanation. For a full list of changes in Parse Server 8, please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md). +This document highlights specific changes related to email verification in Parse Server 8. For a full list of changes, please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md). --- -- [Invalid Link Page Changes](#invalid-link-page-changes) - ---- - -## Invalid Link Page Changes - -In Parse Server 8, the invalid link page will no longer provide the `username` URL parameter. Instead, the URL parameter will now be `expiredToken`. This change affects how expired verification emails are handled. +## Email Verification -### Regenerating a Verification Email Request +In Parse Server 8, the invalid link page will no longer provide the `username` URL parameter. Instead, the URL parameter will now be `token`. This affects how expired verification emails are handled. -To regenerate a verification email request, send a `POST` request to the following endpoint: +To regenerate a verification email, send a `POST` request to the following endpoint: ``` HTTP @@ -25,6 +17,6 @@ Headers: Content-Type: application/x-www-form-urlencoded Body: -expiredToken={token} +token={token} ``` diff --git a/public/de-AT/email_verification_link_expired.html b/public/de-AT/email_verification_link_expired.html index 46833cbe2c..cae39c7a46 100644 --- a/public/de-AT/email_verification_link_expired.html +++ b/public/de-AT/email_verification_link_expired.html @@ -15,7 +15,7 @@

{{appName}}

Expired verification link!

- +
diff --git a/public/de/email_verification_link_expired.html b/public/de/email_verification_link_expired.html index 46833cbe2c..cae39c7a46 100644 --- a/public/de/email_verification_link_expired.html +++ b/public/de/email_verification_link_expired.html @@ -15,7 +15,7 @@

{{appName}}

Expired verification link!

- +
diff --git a/public/email_verification_link_expired.html b/public/email_verification_link_expired.html index 46833cbe2c..cae39c7a46 100644 --- a/public/email_verification_link_expired.html +++ b/public/email_verification_link_expired.html @@ -15,7 +15,7 @@

{{appName}}

Expired verification link!

- +
diff --git a/public_html/invalid_verification_link.html b/public_html/invalid_verification_link.html index b61dfdf655..063ac354f4 100644 --- a/public_html/invalid_verification_link.html +++ b/public_html/invalid_verification_link.html @@ -47,8 +47,8 @@ window.onload = addDataToForm; function addDataToForm() { - const expiredToken = getUrlParameter("expiredToken"); - document.getElementById("expiredToken").value = expiredToken; + const token = getUrlParameter("token"); + document.getElementById("token").value = token; var appId = getUrlParameter("appId"); document.getElementById("resendForm").action = '/apps/' + appId + '/resend_verification_email' @@ -60,7 +60,7 @@

Invalid Verification Link

- +
diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 0858b396fc..ec3d7b8ec0 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -39,8 +39,10 @@ describe('Email Verification Token Expiration: ', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); + const url = new URL(sendEmailOptions.link); + const token = url.searchParams.get('token'); expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=${sendEmailOptions.link.split('token=')[1]}` + `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}` ); done(); }); @@ -332,7 +334,7 @@ describe('Email Verification Token Expiration: ', () => { method: 'GET', }); expect(res.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=${token}` + `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}` ); const formUrl = `http://localhost:8378/1/apps/test/resend_verification_email`; @@ -340,7 +342,7 @@ describe('Email Verification Token Expiration: ', () => { url: formUrl, method: 'POST', body: { - expiredToken: token, + token: token, }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirects: false, @@ -672,8 +674,10 @@ describe('Email Verification Token Expiration: ', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); + const url = new URL(sendEmailOptions.link); + const token = url.searchParams.get('token'); expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=${sendEmailOptions.link.split('token=')[1]}` + `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}` ); done(); }); @@ -725,8 +729,10 @@ describe('Email Verification Token Expiration: ', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); + const url = new URL(sendEmailOptions.link); + const token = url.searchParams.get('token'); expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=${sendEmailOptions.link.split('token=')[1]}` + `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}` ); done(); }); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 9546e49d6f..3f6d4048c5 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -740,7 +740,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/invalid_verification_link.html?appId=test&expiredToken=asdfasdf' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=asdfasdf' ); done(); }); @@ -785,7 +785,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/invalid_verification_link.html?appId=test&expiredToken=invalid' + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=invalid' ); user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); @@ -1198,14 +1198,14 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }, }); expect(res.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/choose_password?id=test&error=The%20password%20reset%20link%20has%20expired&app=emailVerifyToken&expiredToken=${token}` + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?id=test&error=The%20password%20reset%20link%20has%20expired&app=emailVerifyToken&token=${token}` ); await request({ url: `http://localhost:8378/1/requestPasswordReset`, method: 'POST', body: { - expiredToken: token, + token: token, }, headers: { 'X-Parse-Application-Id': 'test', diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 0358f4fc9a..455ec038d0 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -217,8 +217,8 @@ export class UserController extends AdaptableController { return this.config.database.update('_User', { username: user.username }, user); } - async resendVerificationEmail(username, req, expiredToken) { - const aUser = await this.getUserIfNeeded({ username, _email_verify_token: expiredToken }); + async resendVerificationEmail(username, req, token) { + const aUser = await this.getUserIfNeeded({ username, _email_verify_token: token }); if (!aUser || aUser.emailVerified) { throw undefined; } diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 4cdbf7c1d3..32dfd1ce09 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -45,7 +45,6 @@ const pages = Object.freeze({ const pageParams = Object.freeze({ appName: 'appName', appId: 'appId', - expiredToken: 'expiredToken', token: 'token', username: 'username', error: 'error', @@ -109,19 +108,19 @@ export class PagesRouter extends PromiseRouter { resendVerificationEmail(req) { const config = req.config; const username = req.body.username; - const expiredToken = req.body.expiredToken; + const token = req.body.token; if (!config) { this.invalidRequest(); } - if (!username && !expiredToken) { + if (!username && !token) { return this.goToPage(req, pages.emailVerificationLinkInvalid); } const userController = config.userController; - return userController.resendVerificationEmail(username, req, expiredToken).then( + return userController.resendVerificationEmail(username, req, token).then( () => { return this.goToPage(req, pages.emailVerificationSendSuccess); }, @@ -233,7 +232,7 @@ export class PagesRouter extends PromiseRouter { if (result?.err === 'The password reset link has expired') { delete query[pageParams.token]; - query[pageParams.expiredToken] = token; + query[pageParams.token] = token; } const page = result.success ? pages.passwordResetSuccess : pages.passwordReset; diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 7a21e760ed..1a09db8596 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -64,15 +64,15 @@ export class PublicAPIRouter extends PromiseRouter { return this.missingPublicServerURL(); } - const expiredToken = req.body.expiredToken; + const token = req.body.token; - if (!username && !expiredToken) { + if (!username && !token) { return this.invalidLink(req); } const userController = config.userController; - return userController.resendVerificationEmail(username, req, expiredToken).then( + return userController.resendVerificationEmail(username, req, token).then( () => { return Promise.resolve({ status: 302, @@ -202,7 +202,7 @@ export class PublicAPIRouter extends PromiseRouter { if (result?.err === 'The password reset link has expired') { delete queryString.token; - queryString.expiredToken = token; + queryString.token = token; } const params = qs.stringify(queryString); @@ -236,12 +236,12 @@ export class PublicAPIRouter extends PromiseRouter { }); } - invalidVerificationLink(req, expiredToken) { + invalidVerificationLink(req, token) { const config = req.config; if (req.params.appId) { const params = qs.stringify({ appId: req.params.appId, - expiredToken, + token, }); return Promise.resolve({ status: 302, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 5d79bd68e2..c3e86a8e4b 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -439,13 +439,13 @@ export class UsersRouter extends ClassesRouter { this._throwOnBadEmailConfig(req); let email = req.body.email; - const expiredToken = req.body.expiredToken; - if (!email && !expiredToken) { + const token = req.body.token; + if (!email && !token) { throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email'); } - if (expiredToken) { + if (token) { const results = await req.config.database.find('_User', { - _perishable_token: expiredToken, + _perishable_token: token, _perishable_token_expires_at: { $lt: Parse._encode(new Date()) }, }); if (results && results[0] && results[0].email) { From e6b67ed57c8ebd57cf47d09b95109e108a54b9a1 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:37:49 +0100 Subject: [PATCH 22/27] Update 8.0.0.md --- 8.0.0.md | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/8.0.0.md b/8.0.0.md index 3bb9633a54..ee18b4d414 100644 --- a/8.0.0.md +++ b/8.0.0.md @@ -1,22 +1,17 @@ -This document highlights specific changes related to email verification in Parse Server 8. For a full list of changes, please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md). +# Parse Server 8 Migration Guide ---- +This document only highlights specific changes that require a longer explanation. For a full list of changes in Parse Server 8 please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md). -## Email Verification +--- -In Parse Server 8, the invalid link page will no longer provide the `username` URL parameter. Instead, the URL parameter will now be `token`. This affects how expired verification emails are handled. +## Email Verification -To regenerate a verification email, send a `POST` request to the following endpoint: +In order to remove sensitive information (PII) from technical logs, the `Parse.User.username` field has been removed from the email verification process. This means the username will no longer be part of URLs related to email verification. A token that is internal to Parse Server and associated with the user will be used instead. -``` -HTTP -Method: POST -URL: {{server url}}/resend_verification_email +This affects how expired verification emails are handled. When opening a verification link that contains an expired token, the page that the user is redirected to will no longer provide the `username` as an URL query parameter. Instead, the URL query parameter `token` will be provided. -Headers: -Content-Type: application/x-www-form-urlencoded +The request to re-trigger sending an email verification email changed sending a `POST` request to the endpoint `/resend_verification_email` with the `token` in the body. -Body: -token={token} -``` +Related pull requests: +- https://github.com/parse-community/parse-server/pull/8488 From 780bc48d1b2f60392d702c18ce1ce0b60431086a Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:40:24 +0100 Subject: [PATCH 23/27] Update 8.0.0.md --- 8.0.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/8.0.0.md b/8.0.0.md index ee18b4d414..2daba715a5 100644 --- a/8.0.0.md +++ b/8.0.0.md @@ -10,7 +10,7 @@ In order to remove sensitive information (PII) from technical logs, the `Parse.U This affects how expired verification emails are handled. When opening a verification link that contains an expired token, the page that the user is redirected to will no longer provide the `username` as an URL query parameter. Instead, the URL query parameter `token` will be provided. -The request to re-trigger sending an email verification email changed sending a `POST` request to the endpoint `/resend_verification_email` with the `token` in the body. +The request to re-trigger sending an email verification email changed sending a `POST` request to the endpoint `/resend_verification_email` with the `token` in the body. If you have customized the HTML pages for the PagesRouter in `/public/` or the legacy `PublicAPIRouter` in `/public_html/`, you need to adapt the form request in your custom page. See the example pages in these directories for how the forms must be set up. Related pull requests: From 42a2ff527673ce837e0ae12c07e6f2a0ea21cb14 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:40:42 +0100 Subject: [PATCH 24/27] Update 8.0.0.md --- 8.0.0.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/8.0.0.md b/8.0.0.md index 2daba715a5..f313b5abce 100644 --- a/8.0.0.md +++ b/8.0.0.md @@ -4,6 +4,10 @@ This document only highlights specific changes that require a longer explanation --- +- [Email Verification](#email-verification) + +--- + ## Email Verification In order to remove sensitive information (PII) from technical logs, the `Parse.User.username` field has been removed from the email verification process. This means the username will no longer be part of URLs related to email verification. A token that is internal to Parse Server and associated with the user will be used instead. From a32d0e6cc94d5cf7696e3702885846ebed05fc9e Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:49:46 +0100 Subject: [PATCH 25/27] Update 8.0.0.md --- 8.0.0.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/8.0.0.md b/8.0.0.md index f313b5abce..0ade0d24fd 100644 --- a/8.0.0.md +++ b/8.0.0.md @@ -12,9 +12,12 @@ This document only highlights specific changes that require a longer explanation In order to remove sensitive information (PII) from technical logs, the `Parse.User.username` field has been removed from the email verification process. This means the username will no longer be part of URLs related to email verification. A token that is internal to Parse Server and associated with the user will be used instead. -This affects how expired verification emails are handled. When opening a verification link that contains an expired token, the page that the user is redirected to will no longer provide the `username` as an URL query parameter. Instead, the URL query parameter `token` will be provided. +This affects how expired verification emails are handled. When opening a verification link that contains an expired token, the page that the user is redirected to will no longer provide the `username` as an URL query parameter. Instead, the URL query parameter `token` will be provided. This is the expired email verification token that despite being expired is still stored by Parse Server and is used to identify the user. -The request to re-trigger sending an email verification email changed sending a `POST` request to the endpoint `/resend_verification_email` with the `token` in the body. If you have customized the HTML pages for the PagesRouter in `/public/` or the legacy `PublicAPIRouter` in `/public_html/`, you need to adapt the form request in your custom page. See the example pages in these directories for how the forms must be set up. +The request to re-trigger sending an email verification email changed sending a `POST` request to the endpoint `/resend_verification_email` with `token` in the body, instead of `username`. If you have customized the HTML pages for the `PagesRouter` in `/public/` or the deprecated `PublicAPIRouter` in `/public_html/`, you need to adapt the form request in your custom page. See the example pages in these directories for how the forms must be set up. + +> [!WARNING] +> An expired email verification token is not automatically deleted by Parse Server even though it has expired. If you have implemented a custom clean-up logic that removes expired tokens, this will break the form request to re-trigger sending an email verification as the expired token won't be found and cannot be associated with any user. In that case you'll have to implement your custom process to re-trigger sending an email verification email. Related pull requests: From e75113c1a754ffcd0b78834584cc74ea08700f09 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:06:28 +0100 Subject: [PATCH 26/27] Update 8.0.0.md --- 8.0.0.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/8.0.0.md b/8.0.0.md index 0ade0d24fd..3ac75ce25f 100644 --- a/8.0.0.md +++ b/8.0.0.md @@ -10,14 +10,14 @@ This document only highlights specific changes that require a longer explanation ## Email Verification -In order to remove sensitive information (PII) from technical logs, the `Parse.User.username` field has been removed from the email verification process. This means the username will no longer be part of URLs related to email verification. A token that is internal to Parse Server and associated with the user will be used instead. +In order to remove sensitive information (PII) from technical logs, the `Parse.User.username` field has been removed from the email verification process. This means the username will no longer be used and the already existing verification token, that is internal to Parse Server and associated with the user, will be used instead. This makes use of the fact that an expired verification token is not deleted from the database by Parse Server, despite being expired, and can therefore be used to identify a user. -This affects how expired verification emails are handled. When opening a verification link that contains an expired token, the page that the user is redirected to will no longer provide the `username` as an URL query parameter. Instead, the URL query parameter `token` will be provided. This is the expired email verification token that despite being expired is still stored by Parse Server and is used to identify the user. +This change affects how verification emails with expired tokens are handled. When opening a verification link that contains an expired token, the page that the user is redirected to will no longer provide the `username` as a URL query parameter. Instead, the URL query parameter `token` will be provided. -The request to re-trigger sending an email verification email changed sending a `POST` request to the endpoint `/resend_verification_email` with `token` in the body, instead of `username`. If you have customized the HTML pages for the `PagesRouter` in `/public/` or the deprecated `PublicAPIRouter` in `/public_html/`, you need to adapt the form request in your custom page. See the example pages in these directories for how the forms must be set up. +The request to re-send a verification email changed to sending a `POST` request to the endpoint `/resend_verification_email` with `token` in the body, instead of `username`. If you have customized the HTML pages for email verification either for the `PagesRouter` in `/public/` or the deprecated `PublicAPIRouter` in `/public_html/`, you need to adapt the form request in your custom pages. See the example pages in these aforementioned directories for how the forms must be set up. > [!WARNING] -> An expired email verification token is not automatically deleted by Parse Server even though it has expired. If you have implemented a custom clean-up logic that removes expired tokens, this will break the form request to re-trigger sending an email verification as the expired token won't be found and cannot be associated with any user. In that case you'll have to implement your custom process to re-trigger sending an email verification email. +> An expired verification token is not automatically deleted from the database by Parse Server even though it has expired. If you have implemented a custom clean-up logic that removes expired tokens, this will break the form request to re-send a verification email as the expired token won't be found and cannot be associated with any user. In that case you'll have to implement your custom process to re-send a verification email. Related pull requests: From 02069d9002a016c02b19bf0948d16c5102aef5dd Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 23 Feb 2025 17:27:51 +1100 Subject: [PATCH 27/27] Update 8.0.0.md --- 8.0.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/8.0.0.md b/8.0.0.md index 3ac75ce25f..0b85f1ac31 100644 --- a/8.0.0.md +++ b/8.0.0.md @@ -18,6 +18,7 @@ The request to re-send a verification email changed to sending a `POST` request > [!WARNING] > An expired verification token is not automatically deleted from the database by Parse Server even though it has expired. If you have implemented a custom clean-up logic that removes expired tokens, this will break the form request to re-send a verification email as the expired token won't be found and cannot be associated with any user. In that case you'll have to implement your custom process to re-send a verification email. +> The resend mechanism is only comptabile with the most recent expired database. It is recommended to use `emailVerifyTokenReuseIfValid` to reduce the likelihood of having to deal with expired tokens. Related pull requests: