all files / express-stormpath/lib/controllers/ change-password.js

6.58% Statements 5/76
0% Branches 0/39
0% Functions 0/15
6.58% Lines 5/76
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192                                                                                                                                                                                                                                                                                                                                                                                     
'use strict';
 
var forms = require('../forms');
var helpers = require('../helpers');
var oktaErrorTransformer = require('../okta/error-transformer');
 
/**
 * Uses the AuthN API to complete a password reset workflow.
 *
 * Use application.sendPasswordResetEmail() to get recoveryTokenResponse
 *
 * @param {*} client stormpath client instance
 * @param {*} recoveryTokenResource the response from /authn/recovery/password
 * @param {*} newPassword new password that the end-user has provided
 * @param {*} callback
 */
function resetPasswordWithRecoveryToken(client, recoveryTokenResource, newPassword, callback) {
 
  var userHref = '/users/' + recoveryTokenResource._embedded.user.id;
 
  client.getAccount(userHref, function (err, account) {
 
    if (err) {
      return callback(err);
    }
 
    var href = '/authn/recovery/answer';
    var body = {
      stateToken: recoveryTokenResource.stateToken,
      answer: account.profile.stormpathMigrationRecoveryAnswer
    };
 
    client.createResource(href, body, function (err, result) {
 
      if (err) {
        return callback(err);
      }
 
      var href = '/authn/credentials/reset_password';
      var body = {
        stateToken: result.stateToken,
        newPassword: newPassword
      };
 
      client.createResource(href, body, callback);
 
    });
  });
}
 
/**
 * Allow a user to change their password.
 *
 * This can only happen if a user has reset their password, received the
 * password reset email, then clicked the link in the email which redirects them
 * to this controller.
 *
 * The URL this controller is bound to, and the view used to render this page
 * can all be controlled via express-stormpath settings.
 *
 * @method
 *
 * @param {Object} req - The http request.
 * @param {Object} res - The http response.
 * @param {function} next - Callback to call next middleware.
 */
module.exports = function (req, res, next) {
  var application = req.app.get('stormpathApplication');
  var client = req.app.get('stormpathClient');
  var config = req.app.get('stormpathConfig');
  var logger = req.app.get('stormpathLogger');
  var sptoken = req.query.sptoken || req.body.sptoken;
  var view = config.web.changePassword.view;
 
  if (!sptoken) {
    return res.redirect(config.web.forgotPassword.uri);
  }
 
  helpers.handleAcceptRequest(req, res, {
    'application/json': function () {
      if (req.method !== 'GET' && req.method !== 'POST') {
        return next();
      }
 
      application.verifyPasswordResetToken(sptoken, function (err, result) {
        if (err) {
          logger.info('A user attempted to reset their password with a token, but that token verification failed.');
          err = oktaErrorTransformer(err);
          return helpers.writeJsonError(res, err);
        }
 
        // For GET requests, respond with 200 OK if the token is valid.
        if (req.method === 'GET') {
          return res.end();
        }
 
        return resetPasswordWithRecoveryToken(client, result, req.body.password, function (err) {
          if (err) {
            logger.info('A user attempted to reset their password, but the password change itself failed.');
            err = oktaErrorTransformer(err);
            return helpers.writeJsonError(res, err);
          }
 
          res.end();
        });
      });
    },
    'text/html': function () {
      if (req.method !== 'GET' && req.method !== 'POST') {
        return next();
      }
 
      var formActionUri = config.web.changePassword.uri
        + '?sptoken=' + encodeURIComponent(req.query.sptoken);
 
      return helpers.organizationResolutionGuard(req, res, formActionUri, function () {
        application.verifyPasswordResetToken(sptoken, function (err, result) {
          if (err) {
            logger.info('A user attempted to reset their password with a token, but that token verification failed.');
            return res.redirect(config.web.changePassword.errorUri);
          }
 
          forms.changePasswordForm.handle(req, {
            // If we get here, it means the user is submitting a password change
            // request, so we should attempt to change the user's password.
            success: function (form) {
              var viewData = {
                form: form,
                organization: req.organization
              };
 
              if (form.data.password !== form.data.passwordAgain) {
                viewData.error = 'Passwords do not match.';
                return helpers.render(req, res, view, viewData);
              }
 
              result.password = form.data.password;
 
              return resetPasswordWithRecoveryToken(client, result, form.data.password, function (err) {
                if (err) {
                  logger.info('A user attempted to reset their password, but the password change itself failed.');
                  err = oktaErrorTransformer(err);
                  viewData.error = err.userMessage;
                  return helpers.render(req, res, view, viewData);
                }
 
                if (config.web.changePassword.autoLogin) {
                  var options = {
                    username: result.email,
                    password: result.password
                  };
 
                  if (req.organization) {
                    options.accountStore = req.organization.href;
                  }
 
                  return helpers.authenticate(options, req, res, function (err) {
                    if (err) {
                      viewData.error = err.userMessage;
                      return helpers.render(req, res, view, viewData);
                    }
 
                    res.redirect(config.web.login.nextUri);
                  });
                }
 
                res.redirect(config.web.changePassword.nextUri);
              });
            },
            // If we get here, it means the user didn't supply required form fields.
            error: function (form) {
              // Special case: if the user is being redirected to this page for the
              // first time, don't display any error.
              if (form.data && !form.data.password && !form.data.passwordAgain) {
                return helpers.render(req, res, view, { form: form, organization: req.organization });
              }
 
              var formErrors = helpers.collectFormErrors(form);
              helpers.render(req, res, view, { form: form, formErrors: formErrors });
            },
            // If we get here, it means the user is doing a simple GET request, so we
            // should just render the forgot password template.
            empty: function (form) {
              helpers.render(req, res, view, { form: form, organization: req.organization });
            }
          });
        });
      });
    }
  }, next);
};