Skip to content

Improve reset password API #6830

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

Draft
wants to merge 9 commits into
base: alpha
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 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
260 changes: 258 additions & 2 deletions spec/PublicAPI.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
const req = require('../lib/request');
const Config = require('../lib/Config');

const request = function(url, callback) {
const request = function (url, callback) {
return req({
url,
}).then(response => callback(null, response), err => callback(err, err));
}).then(
response => callback(null, response),
err => callback(err, err)
);
};

describe('public API', () => {
Expand Down Expand Up @@ -208,4 +212,256 @@ describe('public API supplied with invalid application id', () => {
}
);
});

fdescribe('resetPassword', () => {
let makeRequest;
const re = new RegExp('^(?=.*[a-z]).{8,}');
let sendEmailOptions;
const emailAdapter = {
sendVerificationEmail: {},
sendPasswordResetEmail: options => {
sendEmailOptions = options;
},
sendMail: () => {},
};

const serverURL = 'http://localhost:8378/1';

const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo',
};

beforeEach(() => {
makeRequest = reconfigureServer({
appName: 'coolapp',
publicServerURL: 'http://localhost:1337/1',
emailAdapter: emailAdapter,
passwordPolicy: {
validatorPattern: re,
doNotAllowUsername: true,
maxPasswordHistory: 1,
resetTokenValidityDuration: 0.5, // 0.5 second
},
}).then(() => {
const config = Config.get('test');
const user = new Parse.User();
user.setPassword('asdsweqwasas');
user.setUsername('test');
user.set('email', '[email protected]');
return user
.signUp(null)
.then(() => {
// build history
user.setPassword('aaaaaaaaaaaa');
return user.save();
})
.then(() => Parse.User.requestPasswordReset('[email protected]'))
.then(() =>
config.database.adapter.find(
'_User',
{ fields: {} },
{ username: 'test' },
{ limit: 1 }
)
);
});
});

it('Password reset failed due to password policy', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'zxcv',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Parse.Error.VALIDATION_ERROR is generic, there should be another error code like Parse.Error.PASSWORD_POLICY_NOT_MEET
expect(err.data.code).not.toBe(undefined);
expect(err.data.code).toBe(Parse.Error.VALIDATION_ERROR);
done();
}
);
});
});

it('Password reset failed due to invalid token', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'] + 'invalid',
new_password: 'zxcv',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Missing Parse.Error code, only string message, there should be an error code like Parse.Error.RESET_PASSWORD_ERROR
expect(err.data.code).not.toBe(undefined);
done();
}
);
});
});

it('Password reset failed due to password is repeated', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'aaaaaaaaaaaa',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Parse.Error.VALIDATION_ERROR is generic, there should be another error code like Parse.Error.PASSWORD_POLICY_REPEAT
expect(err.data.code).not.toBe(undefined);
expect(err.data.code).toBe(Parse.Error.VALIDATION_ERROR);
done();
}
);
});
});

it('Password reset failed due to it contains username', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'asdsweqwasastest',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Parse.Error.VALIDATION_ERROR is generic, there should be another error code like Parse.Error.PASSWORD_POLICY_USERNAME
expect(err.data.code).not.toBe(undefined);
expect(err.data.code).toBe(Parse.Error.VALIDATION_ERROR);
done();
}
);
});
});

it('Password reset username not found', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test1',
token: results[0]['_perishable_token'],
new_password: 'asdsweqwasastest',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Missing Parse.Error code, only string message, there should be an error code like Parse.Error.USERNAME_NOT_FOUND
expect(err.data.code).not.toBe(undefined);
done();
}
);
});
});

it('Password reset failed due to link has expired', done => {
makeRequest
.then(results => {
// wait for a bit more than the validity duration set
setTimeout(() => {
expect(sendEmailOptions).not.toBeUndefined();

req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'asdsweqwasas',
}),
})
.then(() => {
fail('Expected to be failed');
done();
})
.catch(error => {
// TODO: Missing Parse.Error code, only string message, there should be an error code like Parse.Error.RESET_LINK_EXPIRED
expect(error.data.code).not.toBe(undefined);
expect(error.data.code).toBe(Parse.Error.RESET_LINK_EXPIRED);
});
done();
}, 1000);
})
.catch(err => {
jfail(err);
done();
});
});

it('Password successfully reset', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'asdsweqwasas',
}),
}).then(
res => {
expect(res.status).toBe(200);
done();
},
() => {
fail('Expected to not fail');
done();
}
);
});
});
});
});
15 changes: 9 additions & 6 deletions src/Controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export class UserController extends AdaptableController {
)
.then(results => {
if (results.length != 1) {
throw 'Failed to reset password: username / email / token is invalid';
throw new Parse.Error(
Parse.Error.RESET_PASSWORD_ERROR,
'Failed to reset password: username / email / token is invalid'
);
}

if (
Expand All @@ -102,7 +105,10 @@ export class UserController extends AdaptableController {
expiresDate = new Date(expiresDate.iso);
}
if (expiresDate < new Date())
throw 'The password reset link has expired';
throw new Parse.Error(
Parse.Error.RESET_LINK_EXPIRED,
'The password reset link has expired'
);
}

return results[0];
Expand Down Expand Up @@ -246,10 +252,7 @@ export class UserController extends AdaptableController {
return this.checkResetTokenValidity(username, token)
.then(user => updateUserPassword(user.objectId, password, this.config))
.catch(error => {
if (error && error.message) {
// in case of Parse.Error, fail with the error message only
return Promise.reject(error.message);
} else {
if (error) {
return Promise.reject(error);
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/Routers/PublicAPIRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export class PublicAPIRouter extends PromiseRouter {
username: username,
token: token,
id: config.applicationId,
error: result.err,
error: result.err.message,
app: config.appName,
});

Expand Down
37 changes: 37 additions & 0 deletions src/Routers/UsersRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,40 @@ export class UsersRouter extends ClassesRouter {
}
}

handlePasswordReset(req) {
this._throwOnBadEmailConfig(req);

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

if (!username) {
throw new Parse.Error(
Parse.Error.USERNAME_MISSING,
'you must provide an username'
);
}
return req.config.database
.find('_User', {
username: username,
})
.then(results => {
if (!results.length || results.length < 1) {
throw new Parse.Error(
Parse.Error.USERNAME_NOT_FOUND,
`No user found with ${username}`
);
}

const userController = req.config.userController;
return userController
.updatePassword(username, token, new_password)
.then(() => {
return {
response: {},
};
});
});
}

handleResetRequest(req) {
this._throwOnBadEmailConfig(req);

Expand Down Expand Up @@ -473,6 +507,9 @@ export class UsersRouter extends ClassesRouter {
this.route('POST', '/requestPasswordReset', req => {
return this.handleResetRequest(req);
});
this.route('POST', '/passwordReset', req => {
return this.handlePasswordReset(req);
});
this.route('POST', '/verificationEmailRequest', req => {
return this.handleVerificationEmailRequest(req);
});
Expand Down