Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Remove username from email verification and password reset emails #8488

Open
wants to merge 33 commits into
base: alpha
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
78c67d4
fix: remove username from verification emails
dblythy Mar 30, 2023
407825d
tests
dblythy Mar 30, 2023
0c49a4a
feat: allow Pointers in cloud code params
dblythy Mar 30, 2023
3d50b00
Revert "feat: allow Pointers in cloud code params"
dblythy Mar 30, 2023
713e357
Merge branch 'alpha' into password-reset
dblythy Jul 24, 2023
f814457
wip
dblythy Jul 24, 2023
b960a1a
Update ValidationAndPasswordsReset.spec.js
dblythy Jul 25, 2023
9f3808a
Merge remote-tracking branch 'upstream/alpha' into password-reset
dblythy Jan 28, 2025
9d4a028
Update UserController.spec.js
dblythy Jan 28, 2025
7252893
fix failing tests
dblythy Jan 28, 2025
f9b54dd
Update UserController.js
dblythy Jan 28, 2025
4a72e2e
fix tests
dblythy Jan 28, 2025
e110732
Update ValidationAndPasswordsReset.spec.js
dblythy Jan 28, 2025
5beb30f
Update ValidationAndPasswordsReset.spec.js
dblythy Jan 29, 2025
205d59e
Update UserController.spec.js
dblythy Jan 29, 2025
db5409a
fix failing tests
dblythy Jan 29, 2025
170f83a
add logging
dblythy Jan 29, 2025
6ff9e6b
Update CurrentSpecReporter.js
dblythy Jan 29, 2025
e1ed76b
revert resolve
dblythy Jan 29, 2025
0d8a4a2
add catch
dblythy Jan 29, 2025
493fcf2
Merge branch 'alpha' into password-reset
dblythy Jan 29, 2025
0cb359a
Merge branch 'alpha' into password-reset
mtrezza Jan 30, 2025
3295166
Create 8.0.0.md
dblythy Jan 30, 2025
4808c8a
fix pages router
dblythy Feb 3, 2025
c09bc87
Merge branch 'alpha' into password-reset
mtrezza Feb 3, 2025
d60bbd2
review feedback
dblythy Feb 12, 2025
e6b67ed
Update 8.0.0.md
mtrezza Feb 12, 2025
780bc48
Update 8.0.0.md
mtrezza Feb 12, 2025
42a2ff5
Update 8.0.0.md
mtrezza Feb 12, 2025
a32d0e6
Update 8.0.0.md
mtrezza Feb 12, 2025
ae6ac40
Merge branch 'alpha' into password-reset
mtrezza Feb 12, 2025
e75113c
Update 8.0.0.md
mtrezza Feb 12, 2025
02069d9
Update 8.0.0.md
dblythy Feb 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions 8.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Parse Server 8 Migration Guide <!-- omit in toc -->

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}
```

2 changes: 1 addition & 1 deletion public/de-AT/email_verification_link_expired.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<h1>{{appName}}</h1>
<h1>Expired verification link!</h1>
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
<input name="username" type="hidden" value="{{{username}}}">
<input name="expiredToken" type="hidden" value="{{{expiredToken}}}">
<input name="locale" type="hidden" value="{{{locale}}}">
<button type="submit">Resend Link</button>
</form>
Expand Down
2 changes: 1 addition & 1 deletion public/de/email_verification_link_expired.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<h1>{{appName}}</h1>
<h1>Expired verification link!</h1>
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
<input name="username" type="hidden" value="{{{username}}}">
<input name="expiredToken" type="hidden" value="{{{expiredToken}}}">
<input name="locale" type="hidden" value="{{{locale}}}">
<button type="submit">Resend Link</button>
</form>
Expand Down
2 changes: 1 addition & 1 deletion public/email_verification_link_expired.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<h1>{{appName}}</h1>
<h1>Expired verification link!</h1>
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
<input name="username" type="hidden" value="{{{username}}}">
<input name="expiredToken" type="hidden" value="{{{expiredToken}}}">
<input name="locale" type="hidden" value="{{{locale}}}">
<button type="submit">Resend Link</button>
</form>
Expand Down
6 changes: 3 additions & 3 deletions public_html/invalid_verification_link.html
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -60,7 +60,7 @@
<div class="container">
<h1>Invalid Verification Link</h1>
<form id="resendForm" method="POST" action="/resend_verification_email">
<input id="usernameField" class="form-control" name="username" type="hidden" value="">
<input id="expiredToken" class="form-control" name="expiredToken" type="hidden" value="">
<button type="submit" class="btn btn-default">Resend Link</button>
</form>
</div>
Expand Down
4 changes: 2 additions & 2 deletions spec/AccountLockoutPolicy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down Expand Up @@ -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',
},
Expand Down
66 changes: 62 additions & 4 deletions spec/EmailVerificationToken.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?username=testEmailVerifyTokenValidity&appId=test'
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=${sendEmailOptions.link.split('token=')[1]}`
);
done();
});
Expand Down Expand Up @@ -135,7 +135,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();
});
Expand Down Expand Up @@ -292,6 +292,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', '[email protected]');
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_id('9365c53c-b8b4-41f7-a3c1-77882f76a89c')(it)('can conditionally send emails', async () => {
let sendEmailOptions;
const emailAdapter = {
Expand Down Expand Up @@ -615,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/verify_email_success.html?username=testEmailVerifyTokenValidity'
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=${sendEmailOptions.link.split('token=')[1]}`
);
done();
});
Expand Down Expand Up @@ -668,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?username=testEmailVerifyTokenValidity&appId=test'
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&expiredToken=${sendEmailOptions.link.split('token=')[1]}`
);
done();
});
Expand Down
48 changes: 10 additions & 38 deletions spec/PagesRouter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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',
});
Expand All @@ -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',
});
Expand Down Expand Up @@ -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}`)
Expand All @@ -696,7 +676,6 @@ describe('Pages Router', () => {
body: {
token,
locale,
username,
new_password: 'newPassword',
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
Expand Down Expand Up @@ -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`;
Expand All @@ -810,7 +787,7 @@ describe('Pages Router', () => {
method: 'POST',
body: {
locale,
username,
username: 'exampleUsername',
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
followRedirects: false,
Expand Down Expand Up @@ -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(() =>
Expand All @@ -870,7 +845,7 @@ describe('Pages Router', () => {
method: 'POST',
body: {
locale,
username,
username: 'exampleUsername',
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
followRedirects: false,
Expand Down Expand Up @@ -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();
Expand All @@ -1171,7 +1144,6 @@ describe('Pages Router', () => {
method: 'POST',
body: {
token,
username,
new_password: 'newPassword',
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
Expand Down
2 changes: 1 addition & 1 deletion spec/ParseLiveQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Expand Down
Loading
Loading