'use strict'; const request = require('../lib/request'); describe('Password Policy: ', () => { it('should show the invalid link page if the user clicks on the password reset link after the token expires', done => { const user = new Parse.User(); let sendEmailOptions; const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { sendEmailOptions = options; }, sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', emailAdapter: emailAdapter, passwordPolicy: { resetTokenValidityDuration: 0.5, // 0.5 second }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { user.setUsername('testResetTokenValidity'); user.setPassword('original'); user.set('email', 'user@parse.com'); return user.signUp(); }) .then(() => { Parse.User.requestPasswordReset('user@parse.com').catch(err => { jfail(err); fail('Reset password request should not fail'); done(); }); }) .then(() => { // wait for a bit more than the validity duration set setTimeout(() => { expect(sendEmailOptions).not.toBeUndefined(); request({ url: sendEmailOptions.link, followRedirects: false, simple: false, resolveWithFullResponse: true, }) .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' ); done(); }) .catch(error => { fail(error); }); }, 1000); }) .catch(err => { jfail(err); done(); }); }); it('should show the reset password page if the user clicks on the password reset link before the token expires', done => { const user = new Parse.User(); let sendEmailOptions; const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { sendEmailOptions = options; }, sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', emailAdapter: emailAdapter, passwordPolicy: { resetTokenValidityDuration: 5, // 5 seconds }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { user.setUsername('testResetTokenValidity'); user.setPassword('original'); user.set('email', 'user@parse.com'); return user.signUp(); }) .then(() => { Parse.User.requestPasswordReset('user@parse.com').catch(err => { jfail(err); fail('Reset password request should not fail'); done(); }); }) .then(() => { // wait for a bit but less than the validity duration setTimeout(() => { expect(sendEmailOptions).not.toBeUndefined(); request({ url: sendEmailOptions.link, simple: false, resolveWithFullResponse: true, 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=testResetTokenValidity/; expect(response.text.match(re)).not.toBe(null); done(); }) .catch(error => { fail(error); }); }, 1000); }) .catch(err => { jfail(err); done(); }); }); it('should fail if passwordPolicy.resetTokenValidityDuration is not a number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { resetTokenValidityDuration: 'not a number', }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail( 'passwordPolicy.resetTokenValidityDuration "not a number" test failed' ); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.resetTokenValidityDuration must be a positive number' ); done(); }); }); it('should fail if passwordPolicy.resetTokenValidityDuration is zero or a negative number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { resetTokenValidityDuration: 0, }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail('resetTokenValidityDuration negative number test failed'); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.resetTokenValidityDuration must be a positive number' ); done(); }); }); it('should fail if passwordPolicy.validatorPattern setting is invalid type', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: 1234, // number is not a valid setting }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail('passwordPolicy.validatorPattern type test failed'); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.validatorPattern must be a regex string or RegExp object.' ); done(); }); }); it('should fail if passwordPolicy.validatorCallback setting is invalid type', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorCallback: 'abc', // string is not a valid setting }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail('passwordPolicy.validatorCallback type test failed'); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.validatorCallback must be a function.' ); done(); }); }); it('signup should fail if password does not conform to the policy enforced using validatorPattern', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: /[0-9]+/, // password should contain at least one digit }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('nodigit'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { fail( 'Should have failed as password does not conform to the policy.' ); done(); }) .catch(error => { expect(error.code).toEqual(142); done(); }); }); }); it('signup should fail if password does not conform to the policy enforced using validatorPattern string', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: '^.{8,}', // password should contain at least 8 char }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('less'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { fail( 'Should have failed as password does not conform to the policy.' ); done(); }) .catch(error => { expect(error.code).toEqual(142); done(); }); }); }); it('signup should fail if password is empty', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: '^.{8,}', // password should contain at least 8 char }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword(''); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { fail( 'Should have failed as password does not conform to the policy.' ); done(); }) .catch(error => { expect(error.message).toEqual( 'Cannot sign up user with an empty password.' ); done(); }); }); }); it('signup should succeed if password conforms to the policy enforced using validatorPattern', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: /[0-9]+/, // password should contain at least one digit }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('1digit'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { Parse.User.logOut() .then(() => { Parse.User.logIn('user1', '1digit') .then(function() { done(); }) .catch(err => { jfail(err); fail('Should be able to login'); done(); }); }) .catch(error => { jfail(error); fail('logout should have succeeded'); done(); }); }) .catch(error => { jfail(error); fail( 'Signup should have succeeded as password conforms to the policy.' ); done(); }); }); }); it('signup should succeed if password conforms to the policy enforced using validatorPattern string', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: '[!@#$]+', // password should contain at least one special char }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('p@sswrod'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { Parse.User.logOut() .then(() => { Parse.User.logIn('user1', 'p@sswrod') .then(function() { done(); }) .catch(err => { jfail(err); fail('Should be able to login'); done(); }); }) .catch(error => { jfail(error); fail('logout should have succeeded'); done(); }); }) .catch(error => { jfail(error); fail( 'Signup should have succeeded as password conforms to the policy.' ); done(); }); }); }); it('signup should fail if password does not conform to the policy enforced using validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorCallback: () => false, // just fail }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('any'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { fail( 'Should have failed as password does not conform to the policy.' ); done(); }) .catch(error => { expect(error.code).toEqual(142); done(); }); }); }); it('signup should succeed if password conforms to the policy enforced using validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorCallback: () => true, // never fail }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('oneUpper'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { Parse.User.logOut() .then(() => { Parse.User.logIn('user1', 'oneUpper') .then(function() { done(); }) .catch(err => { jfail(err); fail('Should be able to login'); done(); }); }) .catch(error => { jfail(error); fail('Logout should have succeeded'); done(); }); }) .catch(error => { jfail(error); fail('Should have succeeded as password conforms to the policy.'); done(); }); }); }); it('signup should fail if password does not match validatorPattern but succeeds validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter validatorCallback: () => true, }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('all lower'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { fail( 'Should have failed as password does not conform to the policy.' ); done(); }) .catch(error => { expect(error.code).toEqual(142); done(); }); }); }); it('signup should fail if password matches validatorPattern but fails validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter validatorCallback: () => false, }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('oneUpper'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { fail( 'Should have failed as password does not conform to the policy.' ); done(); }) .catch(error => { expect(error.code).toEqual(142); done(); }); }); }); it('signup should succeed if password conforms to both validatorPattern and validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: /[A-Z]+/, // password should contain at least one digit validatorCallback: () => true, }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('oneUpper'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { Parse.User.logOut() .then(() => { Parse.User.logIn('user1', 'oneUpper') .then(function() { done(); }) .catch(err => { jfail(err); fail('Should be able to login'); done(); }); }) .catch(error => { jfail(error); fail('logout should have succeeded'); done(); }); }) .catch(error => { jfail(error); fail('Should have succeeded as password conforms to the policy.'); done(); }); }); }); it('should reset password if new password conforms to password policy', done => { 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=has2init&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, followRedirects: false, simple: false, resolveWithFullResponse: true, }) .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' ); Parse.User.logIn('user1', 'has2init') .then(function() { done(); }) .catch(err => { jfail(err); fail('should login with new 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: { validatorPattern: /[0-9]+/, // password should contain at least one digit }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('has 1 digit'); 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 fail to reset password if the new password does not conform to password policy', done => { 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=hasnodigit&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, followRedirects: false, simple: false, resolveWithFullResponse: true, }) .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20meet%20the%20Password%20Policy%20requirements.&app=passwordPolicy` ); Parse.User.logIn('user1', 'has 1 digit') .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: { validatorPattern: /[0-9]+/, // password should contain at least one digit }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('has 1 digit'); 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 fail if passwordPolicy.doNotAllowUsername is not a boolean value', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { doNotAllowUsername: 'no', }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail('passwordPolicy.doNotAllowUsername type test failed'); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.doNotAllowUsername must be a boolean value.' ); done(); }); }); it('signup should fail if password contains the username and is not allowed by policy', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: /[0-9]+/, doNotAllowUsername: true, }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('@user11'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { fail('Should have failed as password contains username.'); done(); }) .catch(error => { expect(error.code).toEqual(142); done(); }); }); }); it('signup should succeed if password does not contain the username and is not allowed by policy', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', 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(() => { done(); }) .catch(() => { fail('Should have succeeded as password does not contain username.'); done(); }); }); }); it('signup should succeed if password contains the username and it is allowed by policy', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: /[0-9]+/, }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('user1'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { done(); }) .catch(() => { fail('Should have succeeded as policy allows username in password.'); done(); }); }); }); it('should fail to reset password if the new password contains username and not allowed by password policy', done => { 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', }, followRedirects: false, simple: false, resolveWithFullResponse: true, }) .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20meet%20the%20Password%20Policy%20requirements.&app=passwordPolicy` ); 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 = { 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=uuser11&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, followRedirects: false, simple: false, resolveWithFullResponse: true, }) .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' ); Parse.User.logIn('user1', 'uuser11') .then(function() { done(); }) .catch(err => { jfail(err); fail('should login with new password'); done(); }); }) .catch(error => { jfail(error); fail('Failed to POST request password reset'); }); }) .catch(error => { jfail(error); fail('Failed to get the reset link'); }); }, sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { validatorPattern: /[0-9]+/, doNotAllowUsername: false, }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { user.setUsername('user1'); user.setPassword('has 1 digit'); 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 fail if passwordPolicy.maxPasswordAge is not a number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { maxPasswordAge: 'not a number', }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail('passwordPolicy.maxPasswordAge "not a number" test failed'); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.maxPasswordAge must be a positive number' ); done(); }); }); it('should fail if passwordPolicy.maxPasswordAge is a negative number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { maxPasswordAge: -100, }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail('passwordPolicy.maxPasswordAge negative number test failed'); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.maxPasswordAge must be a positive number' ); done(); }); }); it('should succeed if logged in before password expires', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { maxPasswordAge: 1, // 1 day }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('user1'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { Parse.User.logIn('user1', 'user1') .then(() => { done(); }) .catch(error => { jfail(error); fail('Login should have succeeded before password expiry.'); done(); }); }) .catch(error => { jfail(error); fail('Signup failed.'); done(); }); }); }); it('should fail if logged in after password expires', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('user1'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { // wait for a bit more than the validity duration set setTimeout(() => { Parse.User.logIn('user1', 'user1') .then(() => { fail('logIn should have failed'); done(); }) .catch(error => { expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); expect(error.message).toEqual( 'Your password has expired. Please reset your password.' ); done(); }); }, 1000); }) .catch(error => { jfail(error); fail('Signup failed.'); done(); }); }); }); it('should apply password expiry policy to existing user upon first login after policy is enabled', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('user1'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { Parse.User.logOut() .then(() => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec }, publicServerURL: 'http://localhost:8378/1', }).then(() => { Parse.User.logIn('user1', 'user1') .then(() => { Parse.User.logOut() .then(() => { // wait for a bit more than the validity duration set setTimeout(() => { Parse.User.logIn('user1', 'user1') .then(() => { fail('logIn should have failed'); done(); }) .catch(error => { expect(error.code).toEqual( Parse.Error.OBJECT_NOT_FOUND ); expect(error.message).toEqual( 'Your password has expired. Please reset your password.' ); done(); }); }, 2000); }) .catch(error => { jfail(error); fail('logout should have succeeded'); done(); }); }) .catch(error => { jfail(error); fail('Login failed.'); done(); }); }); }) .catch(error => { jfail(error); fail('logout should have succeeded'); done(); }); }) .catch(error => { jfail(error); fail('Signup failed.'); done(); }); }); }); it('should reset password timestamp when password is reset', done => { 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=uuser11&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, followRedirects: false, simple: false, resolveWithFullResponse: true, }) .then(response => { expect(response.status).toEqual(302); expect(response.text).toEqual( 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' ); Parse.User.logIn('user1', 'uuser11') .then(function() { done(); }) .catch(err => { jfail(err); fail('should login with new password'); done(); }); }) .catch(error => { jfail(error); fail('Failed to POST request password reset'); }); }) .catch(error => { jfail(error); fail('Failed to get the reset link'); }); }, sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', emailAdapter: emailAdapter, passwordPolicy: { maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('user1'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { // wait for a bit more than the validity duration set setTimeout(() => { Parse.User.logIn('user1', 'user1') .then(() => { fail('logIn should have failed'); done(); }) .catch(error => { expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); expect(error.message).toEqual( 'Your password has expired. Please reset your password.' ); Parse.User.requestPasswordReset('user1@parse.com').catch( err => { jfail(err); fail('Reset password request should not fail'); done(); } ); }); }, 1000); }) .catch(error => { jfail(error); fail('Signup failed.'); done(); }); }); }); it('should fail if passwordPolicy.maxPasswordHistory is not a number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { maxPasswordHistory: 'not a number', }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail('passwordPolicy.maxPasswordHistory "not a number" test failed'); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20' ); done(); }); }); it('should fail if passwordPolicy.maxPasswordHistory is a negative number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { maxPasswordHistory: -10, }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail('passwordPolicy.maxPasswordHistory negative number test failed'); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20' ); done(); }); }); it('should fail if passwordPolicy.maxPasswordHistory is greater than 20', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { maxPasswordHistory: 21, }, publicServerURL: 'http://localhost:8378/1', }) .then(() => { fail('passwordPolicy.maxPasswordHistory negative number test failed'); done(); }) .catch(err => { expect(err).toEqual( 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20' ); done(); }); }); it('should fail to reset if the new password is same as the last password', 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=user1/; const match = response.text.match(re); if (!match) { fail('should have a token'); return Promise.reject('Invalid password link'); } return Promise.resolve(match[1]); // token }) .then(token => { return request({ method: 'POST', url: 'http://localhost:8378/1/apps/test/request_password_reset', body: `new_password=user1&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, followRedirects: false, simple: false, resolveWithFullResponse: true, }).then(response => { return [response, token]; }); }) .then(data => { const response = data[0]; const token = data[1]; expect(response.status).toEqual(302); expect(response.text).toEqual( `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy` ); done(); return Promise.resolve(); }) .catch(error => { fail(error); fail('Repeat password test failed'); done(); }); }, sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { maxPasswordHistory: 1, }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('user1'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { return Parse.User.logOut(); }) .then(() => { return Parse.User.requestPasswordReset('user1@parse.com'); }) .catch(error => { jfail(error); fail('SignUp or reset request failed'); done(); }); }); }); it('should fail if the new password is same as the previous one', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, passwordPolicy: { maxPasswordHistory: 5, }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('user1'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { // try to set the same password as the previous one user.setPassword('user1'); return user.save(); }) .then(() => { fail( 'should have failed because the new password is same as the old' ); done(); }) .catch(error => { expect(error.message).toEqual( 'New password should not be the same as last 5 passwords.' ); expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); done(); }); }); }); it('should fail if the new password is same as the 5th oldest one and policy does not allow the previous 5', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, passwordPolicy: { maxPasswordHistory: 5, }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('user1'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { // build history user.setPassword('user2'); return user.save(); }) .then(() => { user.setPassword('user3'); return user.save(); }) .then(() => { user.setPassword('user4'); return user.save(); }) .then(() => { user.setPassword('user5'); return user.save(); }) .then(() => { // set the same password as the initial one user.setPassword('user1'); return user.save(); }) .then(() => { fail( 'should have failed because the new password is same as the old' ); done(); }) .catch(error => { expect(error.message).toEqual( 'New password should not be the same as last 5 passwords.' ); expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); done(); }); }); }); it('should succeed if the new password is same as the 6th oldest one and policy does not allow only previous 5', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, passwordPolicy: { maxPasswordHistory: 5, }, publicServerURL: 'http://localhost:8378/1', }).then(() => { user.setUsername('user1'); user.setPassword('user1'); user.set('email', 'user1@parse.com'); user .signUp() .then(() => { // build history user.setPassword('user2'); return user.save(); }) .then(() => { user.setPassword('user3'); return user.save(); }) .then(() => { user.setPassword('user4'); return user.save(); }) .then(() => { user.setPassword('user5'); return user.save(); }) .then(() => { user.setPassword('user6'); // this pushes initial password out of history return user.save(); }) .then(() => { // set the same password as the initial one user.setPassword('user1'); return user.save(); }) .then(() => { done(); }) .catch(() => { fail( 'should have succeeded because the new password is not in history' ); done(); }); }); }); });