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

Ajax password reset #5332

Merged
merged 22 commits into from
Mar 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 59 additions & 0 deletions spec/PasswordPolicy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,65 @@ describe('Password Policy: ', () => {
});
});

it('Should return error when password violates Password Policy and reset through ajax', async done => {
const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: async options => {
const response = await request({
url: options.link,
followRedirects: false,
simple: false,
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 match = response.text.match(re);
if (!match) {
fail('should have a token');
return;
}
const token = match[1];

try {
await request({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=xuser12&token=${token}&username=user1`,
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":-1,"error":"Password cannot contain your username."}'
);
}
await Parse.User.logIn('user1', 'r@nd0m');
done();
},
sendMail: () => {},
};
await reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
doNotAllowUsername: true,
},
publicServerURL: 'http://localhost:8378/1',
});
user.setUsername('user1');
user.setPassword('r@nd0m');
user.set('email', 'user1@parse.com');
await user.signUp();

await Parse.User.requestPasswordReset('user1@parse.com');
});

it('should reset password even if the new password contains user name while the policy allows', done => {
const user = new Parse.User();
const emailAdapter = {
Expand Down
66 changes: 66 additions & 0 deletions spec/PublicAPI.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,72 @@ 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',
});

try {
await req({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=user1&token=&username=Johnny`,
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":-1,"error":"Missing token"}');
}
});

it('should return missing password error on ajax request without password 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=&token=132414&username=Johnny`,
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":201,"error":"Missing password"}');
}
});

it('should get invalid_link.html', done => {
request(
'http://localhost:8378/1/apps/invalid_link.html',
Expand Down
83 changes: 83 additions & 0 deletions spec/ValidationAndPasswordsReset.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,89 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
});
});

it('should programmatically reset password on ajax request', async done => {
const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: async options => {
const response = await request({
url: options.link,
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 match = response.text.match(re);
if (!match) {
fail('should have a token');
return;
}
const token = match[1];

const resetResponse = await request({
url: 'http://localhost:8378/1/apps/test/request_password_reset',
method: 'POST',
body: { new_password: 'hello', token, username: 'zxcv' },
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
},
followRedirects: false,
});
expect(resetResponse.status).toEqual(200);
expect(resetResponse.text).toEqual('"Password successfully reset"');

await Parse.User.logIn('zxcv', 'hello');
const config = Config.get('test');
const results = await config.database.adapter.find(
'_User',
{ fields: {} },
{ username: 'zxcv' },
{ limit: 1 }
);
// _perishable_token should be unset after reset password
expect(results.length).toEqual(1);
expect(results[0]['_perishable_token']).toEqual(undefined);
done();
},
sendMail: () => {},
};
await reconfigureServer({
appName: 'emailing app',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: 'http://localhost:8378/1',
});
user.setPassword('asdf');
user.setUsername('zxcv');
user.set('email', 'user@parse.com');
await user.signUp();
await Parse.User.requestPasswordReset('user@parse.com');
});

it('should return ajax failure error on ajax request with wrong data provided', async () => {
await reconfigureServer({
publicServerURL: 'http://localhost:8378/1',
});

try {
await request({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=user1&token=12345&username=Johnny`,
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":-1,"error":"Failed to reset password: username / email / token is invalid"}'
);
}
});

it('deletes password reset token on email address change', done => {
reconfigureServer({
appName: 'coolapp',
Expand Down
4 changes: 2 additions & 2 deletions src/Controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class UserController extends AdaptableController {
)
.then(results => {
if (results.length != 1) {
throw undefined;
Copy link
Contributor

@acinader acinader Mar 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did a little git blame sleuthing on this one.

git show 7e868b2

note the comment that used to be there:

       // Trying to verify email when not enabled
       // TODO: Better error here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't even notice that.

throw 'Failed to reset password: username / email / token is invalid';
}

if (
Expand Down Expand Up @@ -246,7 +246,7 @@ export class UserController extends AdaptableController {
return this.checkResetTokenValidity(username, token)
.then(user => updateUserPassword(user.objectId, password, this.config))
.catch(error => {
if (error.message) {
if (error && error.message) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would we be in a catch and have no error? that seems bad.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same feeling. I could back track and see what the underlying cause is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, we shouldn't throw without a message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just committed a fix

// in case of Parse.Error, fail with the error message only
return Promise.reject(error.message);
} else {
Expand Down
62 changes: 48 additions & 14 deletions src/Routers/PublicAPIRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import express from 'express';
import path from 'path';
import fs from 'fs';
import qs from 'querystring';
import { Parse } from 'parse/node';
acinader marked this conversation as resolved.
Show resolved Hide resolved

const public_html = path.resolve(__dirname, '../../public_html');
const views = path.resolve(__dirname, '../../views');
Expand Down Expand Up @@ -159,34 +160,67 @@ export class PublicAPIRouter extends PromiseRouter {

const { username, token, new_password } = req.body;

if (!username || !token || !new_password) {
if ((!username || !token || !new_password) && req.xhr === false) {
return this.invalidLink(req);
}

if (!username) {
throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'Missing username');
}

if (!token) {
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing token');
}

if (!new_password) {
throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'Missing password');
}

return config.userController
.updatePassword(username, token, new_password)
.then(
() => {
const params = qs.stringify({ username: username });
return Promise.resolve({
status: 302,
location: `${config.passwordResetSuccessURL}?${params}`,
success: true,
});
},
err => {
const params = qs.stringify({
username: username,
token: token,
id: config.applicationId,
error: err,
app: config.appName,
});
return Promise.resolve({
status: 302,
location: `${config.choosePasswordURL}?${params}`,
success: false,
err,
});
}
);
)
.then(result => {
const params = qs.stringify({
username: username,
token: token,
id: config.applicationId,
error: result.err,
app: config.appName,
});

if (req.xhr) {
if (result.success) {
return Promise.resolve({
status: 200,
response: 'Password successfully reset',
});
}
if (result.err) {
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${result.err}`);
dplewis marked this conversation as resolved.
Show resolved Hide resolved
}
}

return Promise.resolve({
status: 302,
location: `${
result.success
? `${config.passwordResetSuccessURL}?username=${username}`
: `${config.choosePasswordURL}?${params}`
}`,
});
});
}

invalidLink(req) {
Expand Down