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 13 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
91 changes: 91 additions & 0 deletions spec/PasswordPolicy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,97 @@ describe('Password Policy: ', () => {
});
});

it('Should return error when password violates Password Policy and reset through ajax', done => {
Copy link
Member

Choose a reason for hiding this comment

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

Can you rewrite your tests using async / await?

Reading this is confusing with all the nested promises.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

async / await is not used in any of the tests, and i noticed there is actually a lot of branching promises in the other tests, since .catch blocks contain specific error messages for almost each action. Is it really necessary to rewrite this test?

const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request({
url: options.link,
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.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 match = response.text.match(re);
if (!match) {
fail('should have a token');
done();
return;
}
const token = match[1];

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 does not meet the Password Policy requirements."}'
);

Parse.User.logIn('user1', 'r@nd0m')
.then(function() {
done();
})
.catch(err => {
jfail(err);
fail('should login with old password');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to POST request password reset');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to get the reset link');
done();
});
},
sendMail: () => {},
};
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
doNotAllowUsername: true,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('r@nd0m');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
jfail(err);
fail('Reset password request should not fail');
done();
});
})
.catch(error => {
jfail(error);
fail('signUp should not fail');
done();
});
});
});

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
71 changes: 71 additions & 0 deletions spec/PublicAPI.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,77 @@ const request = function(url, callback) {
};

describe('public API', () => {
it('should return ajax response on ajax request', done => {
moonion marked this conversation as resolved.
Show resolved Hide resolved
reconfigureServer({
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
return 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"}');
done();
});
});

it('should return missing token error on ajax request without token provided', done => {
reconfigureServer({
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
return 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 => {
console.log('ERROR IN TEST: ', error);
moonion marked this conversation as resolved.
Show resolved Hide resolved
expect(error.status).not.toBe(302);
expect(error.text).toEqual('{"code":-1,"error":"Missing token"}');
done();
});
});

it('should return missing password error on ajax request without password provided', done => {
reconfigureServer({
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
return 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 => {
console.log('ERROR IN TEST: ', error);
moonion marked this conversation as resolved.
Show resolved Hide resolved
expect(error.status).not.toBe(302);
expect(error.text).toEqual('{"code":201,"error":"Missing password"}');
done();
});
});

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

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

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,
}).then(response => {
console.log('REQUEST RESPONSE');
moonion marked this conversation as resolved.
Show resolved Hide resolved
expect(response.status).toEqual(200);
expect(response.text).toEqual('"Password successfully reset"');

Parse.User.logIn('zxcv', 'hello').then(
function() {
const config = Config.get('test');
config.database.adapter
.find(
'_User',
{ fields: {} },
{ username: 'zxcv' },
{ limit: 1 }
)
.then(results => {
// _perishable_token should be unset after reset password
expect(results.length).toEqual(1);
expect(results[0]['_perishable_token']).toEqual(undefined);
dplewis marked this conversation as resolved.
Show resolved Hide resolved
done();
});
},
err => {
jfail(err);
fail('should login with new password');
done();
}
);
});
});
},
sendMail: () => {},
};
reconfigureServer({
appName: 'emailing app',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setPassword('asdf');
user.setUsername('zxcv');
user.set('email', 'user@parse.com');
user.signUp().then(() => {
Parse.User.requestPasswordReset('user@parse.com', {
error: err => {
jfail(err);
fail('Should not fail');
done();
},
});
});
});
});

it('should return ajax failure error on ajax request with wrong data provided', done => {
reconfigureServer({
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
return 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 or token is invalid)"}'
);
done();
});
});

it('deletes password reset token on email address change', done => {
reconfigureServer({
appName: 'coolapp',
Expand Down
70 changes: 55 additions & 15 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 @@ -152,41 +153,80 @@ export class PublicAPIRouter extends PromiseRouter {
if (!config) {
this.invalidRequest();
}

if (!config.publicServerURL) {
return this.missingPublicServerURL();
}

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 ===
'Password does not meet the Password Policy requirements.'
)
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `${result.err}`);
dplewis marked this conversation as resolved.
Show resolved Hide resolved
throw new Parse.Error(
Parse.Error.OTHER_CAUSE,
'Failed to reset password (Username/email or token is invalid)'
);
}

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

invalidLink(req) {
Expand Down