diff --git a/8.0.0.md b/8.0.0.md
new file mode 100644
index 0000000000..3d7dd9d6e2
--- /dev/null
+++ b/8.0.0.md
@@ -0,0 +1,27 @@
+# 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](#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 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 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-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 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.
+
+> [!IMPORTANT]
+> Parse Server does not keep a history of verification tokens but only stores the most recently generated verification token in the database. Every time Parse Server generates a new verification token, the currently stored token is replaced. If a user opens a link with an expired token, and that token has already been replaced in the database, Parse Server cannot associate the expired token with any user. In this case, another way has to be offered to the user to re-send a verification email. To mitigate this issue, set the Parse Server option `emailVerifyTokenReuseIfValid: true` and set `emailVerifyTokenValidityDuration` to a longer duration, which ensures that the currently stored verification token is not replaced too soon.
+
+Related pull requests:
+
+- https://github.com/parse-community/parse-server/pull/8488
diff --git a/public/de-AT/email_verification_link_expired.html b/public/de-AT/email_verification_link_expired.html
index bea8d949fb..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 bea8d949fb..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 bea8d949fb..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 fe6914fc82..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() {
- var username = getUrlParameter("username");
- document.getElementById("usernameField").value = username;
+ 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/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 1e9f6a7830..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?username=testEmailVerifyTokenValidity&appId=test'
+ `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
);
done();
});
@@ -135,7 +137,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();
});
@@ -292,6 +294,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&token=${token}`
+ );
+
+ const formUrl = `http://localhost:8378/1/apps/test/resend_verification_email`;
+ const formResponse = await request({
+ url: formUrl,
+ method: 'POST',
+ body: {
+ token: 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_id('9365c53c-b8b4-41f7-a3c1-77882f76a89c')(it)('can conditionally send emails', async () => {
let sendEmailOptions;
const emailAdapter = {
@@ -614,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/verify_email_success.html?username=testEmailVerifyTokenValidity'
+ `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
);
done();
});
@@ -667,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?username=testEmailVerifyTokenValidity&appId=test'
+ `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
);
done();
});
diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js
index ca61fa4f5a..0aa5bb357b 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',
});
@@ -676,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}`)
@@ -696,7 +676,6 @@ describe('Pages Router', () => {
body: {
token,
locale,
- username,
new_password: 'newPassword',
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -793,15 +772,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`;
@@ -810,7 +787,7 @@ describe('Pages Router', () => {
method: 'POST',
body: {
locale,
- username,
+ username: 'exampleUsername',
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
followRedirects: false,
@@ -847,17 +824,15 @@ describe('Pages Router', () => {
const appId = linkResponse.headers['x-parse-page-param-appid'];
const locale = linkResponse.headers['x-parse-page-param-locale'];
- const username = linkResponse.headers['x-parse-page-param-username'];
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
await jasmine.timeout();
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(() =>
@@ -870,7 +845,7 @@ describe('Pages Router', () => {
method: 'POST',
body: {
locale,
- username,
+ username: 'exampleUsername',
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
followRedirects: false,
@@ -1155,12 +1130,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();
@@ -1171,7 +1144,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 a65eef6084..6294c609a1 100644
--- a/spec/ParseLiveQuery.spec.js
+++ b/spec/ParseLiveQuery.spec.js
@@ -969,7 +969,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/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js
index b39790cfba..1fd2e6aa50 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 9dc8eb8d89..8418494bac 100644
--- a/spec/RegexVulnerabilities.spec.js
+++ b/spec/RegexVulnerabilities.spec.js
@@ -95,7 +95,7 @@ describe('Regex Vulnerabilities', () => {
it('should not work with regex', async () => {
expect(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 user.fetch({ useMasterKey: true });
@@ -117,7 +117,7 @@ describe('Regex Vulnerabilities', () => {
}).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 user.fetch({ useMasterKey: true });
@@ -144,7 +144,7 @@ describe('Regex Vulnerabilities', () => {
});
await 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);
@@ -192,7 +192,7 @@ describe('Regex Vulnerabilities', () => {
}).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 e240d4666a..31d5051960 100644
--- a/spec/UserController.spec.js
+++ b/spec/UserController.spec.js
@@ -19,7 +19,6 @@ describe('UserController', () => {
let emailOptions;
emailAdapter.sendVerificationEmail = options => {
emailOptions = options;
- return Promise.resolve();
};
const username = 'verificationUser';
@@ -35,7 +34,8 @@ describe('UserController', () => {
const rawToken = rawUser[0]._email_verify_token;
expect(rawToken).toBeDefined();
expect(rawUsername).toBe(username);
- 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}`);
});
});
@@ -54,7 +54,6 @@ describe('UserController', () => {
let emailOptions;
emailAdapter.sendVerificationEmail = options => {
emailOptions = options;
- return Promise.resolve();
};
const username = 'verificationUser';
@@ -70,7 +69,8 @@ describe('UserController', () => {
const rawToken = rawUser[0]._email_verify_token;
expect(rawToken).toBeDefined();
expect(rawUsername).toBe(username);
- 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}`);
});
});
});
diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js
index 7c5334261a..3f6d4048c5 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 => {
@@ -334,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);
@@ -675,7 +676,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()
@@ -734,12 +735,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&token=asdfasdf'
);
done();
});
@@ -779,12 +780,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&token=invalid'
);
user.fetch().then(() => {
expect(user.get('emailVerified')).toEqual(false);
@@ -824,7 +825,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();
});
@@ -864,8 +865,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);
@@ -887,7 +887,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');
@@ -907,7 +907,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(
@@ -964,7 +964,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');
@@ -984,7 +984,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();
});
@@ -1023,7 +1023,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');
@@ -1081,7 +1081,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',
@@ -1150,6 +1150,80 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
});
});
+ it('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&token=${token}`
+ );
+
+ await request({
+ url: `http://localhost:8378/1/requestPasswordReset`,
+ method: 'POST',
+ body: {
+ token: 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/spec/helper.js b/spec/helper.js
index 7093cfcc4c..bc20ebcf5e 100644
--- a/spec/helper.js
+++ b/spec/helper.js
@@ -227,65 +227,55 @@ beforeAll(async () => {
Parse.serverURL = 'http://localhost:' + port + '/1';
});
-afterEach(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(global.afterEachFn);
afterAll(() => {
global.displayTestStats();
diff --git a/spec/support/CurrentSpecReporter.js b/spec/support/CurrentSpecReporter.js
index 35c0340dd3..8e0e0dafd1 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) {
diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js
index ac896c51c2..455ec038d0 100644
--- a/src/Controllers/UserController.js
+++ b/src/Controllers/UserController.js
@@ -60,14 +60,14 @@ export class UserController extends AdaptableController {
return true;
}
- async 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' },
@@ -82,50 +82,45 @@ export class UserController extends AdaptableController {
updateFields._email_verify_token_expires_at = { __op: 'Delete' };
}
const maintenanceAuth = Auth.maintenance(this.config);
- var findUserForEmailVerification = await RestQuery({
+ const restQuery = await RestQuery({
method: RestQuery.Method.get,
config: this.config,
auth: maintenanceAuth,
className: '_User',
- restWhere: {
- 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);
+ restWhere: query,
});
+
+ const result = await restQuery.execute();
+ if (result.results.length) {
+ 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];
}
async getUserIfNeeded(user) {
@@ -136,6 +131,9 @@ 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;
+ }
var query = await RestQuery({
method: RestQuery.Method.get,
@@ -173,9 +171,7 @@ export class UserController extends AdaptableController {
if (!shouldSendEmail) {
return;
}
- const username = encodeURIComponent(fetchedUser.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,
@@ -221,8 +217,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, token) {
+ const aUser = await this.getUserIfNeeded({ username, _email_verify_token: token });
if (!aUser || aUser.emailVerified) {
throw undefined;
}
@@ -286,9 +282,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,
@@ -304,21 +299,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 }) {
@@ -368,17 +362,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 018e46e63d..2f59081a03 100644
--- a/src/GraphQL/loaders/usersMutations.js
+++ b/src/GraphQL/loaders/usersMutations.js
@@ -302,11 +302,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');
}
@@ -315,7 +312,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 79a487b6e4..32dfd1ce09 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.emailVerificationLinkInvalid);
}
);
}
@@ -114,18 +108,19 @@ export class PagesRouter extends PromiseRouter {
resendVerificationEmail(req) {
const config = req.config;
const username = req.body.username;
+ const token = req.body.token;
if (!config) {
this.invalidRequest();
}
- if (!username) {
+ if (!username && !token) {
return this.goToPage(req, pages.emailVerificationLinkInvalid);
}
const userController = config.userController;
- return userController.resendVerificationEmail(username, req).then(
+ return userController.resendVerificationEmail(username, req, token).then(
() => {
return this.goToPage(req, pages.emailVerificationSendSuccess);
},
@@ -154,28 +149,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 +178,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 +194,7 @@ export class PagesRouter extends PromiseRouter {
}
return config.userController
- .updatePassword(username, token, new_password)
+ .updatePassword(token, new_password)
.then(
() => {
return Promise.resolve({
@@ -235,16 +222,18 @@ 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,
[pageParams.appName]: config.appName,
};
+
+ if (result?.err === 'The password reset link has expired') {
+ delete query[pageParams.token];
+ query[pageParams.token] = 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 625b3d458a..1a09db8596 100644
--- a/src/Routers/PublicAPIRouter.js
+++ b/src/Routers/PublicAPIRouter.js
@@ -19,7 +19,7 @@ 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;
@@ -33,21 +33,20 @@ 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}`,
});
},
() => {
- return this.invalidVerificationLink(req);
+ return this.invalidVerificationLink(req, token);
}
);
}
@@ -65,13 +64,15 @@ export class PublicAPIRouter extends PromiseRouter {
return this.missingPublicServerURL();
}
- if (!username) {
+ const token = req.body.token;
+
+ if (!username && !token) {
return this.invalidLink(req);
}
const userController = config.userController;
- return userController.resendVerificationEmail(username, req).then(
+ return userController.resendVerificationEmail(username, req, token).then(
() => {
return Promise.resolve({
status: 302,
@@ -125,19 +126,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({
@@ -162,17 +162,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');
}
@@ -182,7 +178,7 @@ export class PublicAPIRouter extends PromiseRouter {
}
return config.userController
- .updatePassword(username, token, new_password)
+ .updatePassword(token, new_password)
.then(
() => {
return Promise.resolve({
@@ -197,13 +193,18 @@ export class PublicAPIRouter extends PromiseRouter {
}
)
.then(result => {
- const params = qs.stringify({
- username: username,
+ 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.token = token;
+ }
+ const params = qs.stringify(queryString);
if (req.xhr) {
if (result.success) {
@@ -217,9 +218,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({
@@ -236,12 +236,12 @@ export class PublicAPIRouter extends PromiseRouter {
});
}
- invalidVerificationLink(req) {
+ invalidVerificationLink(req, token) {
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,
+ token,
});
return Promise.resolve({
status: 302,
diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js
index 70085f988c..c3e86a8e4b 100644
--- a/src/Routers/UsersRouter.js
+++ b/src/Routers/UsersRouter.js
@@ -438,10 +438,20 @@ export class UsersRouter extends ClassesRouter {
async handleResetRequest(req) {
this._throwOnBadEmailConfig(req);
- const { email } = req.body;
- if (!email) {
+ let email = req.body.email;
+ const token = req.body.token;
+ if (!email && !token) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
}
+ if (token) {
+ const results = await req.config.database.find('_User', {
+ _perishable_token: token,
+ _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,