diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 19c0731f12..d4562003b0 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -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 = { diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 1c04294880..22c1383d5a 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -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', diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index a5e024168a..0e9db70a4f 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -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', diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 6b9587182c..2d7b444428 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -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 ( @@ -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 { diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 15d636c5e2..efa0ea5852 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -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'); @@ -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) {