Skip to content

Commit

Permalink
Ajax password reset (parse-community#5332)
Browse files Browse the repository at this point in the history
* adapted public api route for use with ajax

* Elegant error handling

* Fixed error return

* Public API error flow redone, tests

* Fixed code to pre-build form

* Public API change password return params

* Reverted errors in resetPassword

* Fixed querystring call

* Success test on ajax password reset

* Added few more routes to tests for coverage

* More tests and redone error return slightly

* Updated error text

* Console logs removal, renamed test, added {} to if

* Wrong error sent

* Revert changes

* Revert "Revert changes"

This reverts commit 68ee2c4.

* real revert of {}

* nits and test fix

* fix tests

* throw proper error
  • Loading branch information
moonion authored and dplewis committed Mar 14, 2019
1 parent 7c772c4 commit 604e853
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 16 deletions.
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;
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) {
// 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';

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

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

invalidLink(req) {
Expand Down

0 comments on commit 604e853

Please sign in to comment.