You are here

password_strength.module in Password Strength 7

Provides password controls, validation, and strength checker.

File

password_strength.module
View source
<?php

/**
 * @file
 * Provides password controls, validation, and strength checker.
 */
define('PASSWORD_STRENGTH_SCORE_VERYWEAK', 0);
define('PASSWORD_STRENGTH_SCORE_WEAK', 1);
define('PASSWORD_STRENGTH_SCORE_GOOD', 2);
define('PASSWORD_STRENGTH_SCORE_STRONG', 3);
define('PASSWORD_STRENGTH_SCORE_VERYSTRONG', 4);

/**
 * Implements hook_xautoload().
 */
function password_strength_xautoload($api) {
  $api
    ->namespaceRoot('ZxcvbnPhp', 'lib/zxcvbn-php/src');
}

/**
 * Implements hook_menu().
 */
function password_strength_menu() {
  $items = array();
  $items['system/password-strength-check'] = array(
    'title' => 'Check password',
    'page callback' => 'password_strength_ajax_check',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['admin/config/system/password-strength'] = array(
    'title' => 'Password Strength settings',
    'description' => 'Manage password strength settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'password_strength_settings',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'password_strength.admin.inc',
    'weight' => 20,
  );
  return $items;
}

/**
 * Determine if strength should be checked.
 *
 * @param object $account
 *   User account object to optionally include in deciding to check.
 *
 * @return bool
 */
function password_strength_check_strength($account = NULL) {

  // Do not attempt to check if the password library is not available.
  if (!class_exists('ZxcvbnPhp\\Zxcvbn')) {
    return FALSE;
  }

  // @todo: hook for other modules to determine password strength rules
  return TRUE;
}

/**
 * Implements hook_form_alter().
 */
function password_strength_form_alter(&$form, &$form_state, $form_id) {

  // Include password strength JS checks and validation handlers.
  switch ($form_id) {
    case 'user_register_form':
      if (array_key_exists('pass', $form['account']) && password_strength_check_strength()) {
        password_strength_form_password_add($form['account']['pass'], $form);
      }
      break;
    case 'user_profile_form':
      if (password_strength_check_strength()) {
        password_strength_form_password_add($form['account']['pass'], $form);
      }
      break;
  }
}

/**
 * Implements hook_element_info_alter().
 */
function password_strength_element_info_alter(&$types) {

  // Remove user_form_process_password_confirm process callback from
  // password_confirm element if Password Strength is applicable. This will
  // remove core's password strength checker on password fields.
  if (isset($types['password_confirm']['#process'])) {
    $process = 'user_form_process_password_confirm';
    if (($position = array_search($process, $types['password_confirm']['#process'])) !== FALSE && password_strength_check_strength()) {
      unset($types['password_confirm']['#process'][$position]);
    }
  }
}

/**
 * Form extensions for password strength controls.
 *
 * @param array &$element
 *   The password form element being manipulated.
 * @param array &$form
 *   The form being manipulated.
 */
function password_strength_form_password_add(&$element, &$form) {

  // Attach JS.
  password_strength_form_password_js_attach($element, $form['#user']);
  $form['#validate'][] = 'password_strength_form_password_validate';

  // Run password_submit handler before others.
  $form['#submit'] = array_merge(array(
    'password_strength_form_password_submit',
  ), $form['#submit']);
}

/**
 * Attach JS password check to a form element.
 *
 * @param array &$element
 *   The password form element being manipulated.
 * @param object $account
 *   User account setting password.
 */
function password_strength_form_password_js_attach(&$element, $account) {
  $js_settings = array();
  $key = 'password_strength';
  $score = password_strength_required_score($account);
  $js_settings['policy_score'] = $score;
  $key .= ':' . $account->uid;
  $js_settings['uid'] = $account->uid;
  $js_settings['token'] = drupal_get_token($key);
  $js_settings['secure_base_url'] = url(NULL, array(
    'absolute' => TRUE,
    'https' => TRUE,
    'purl' => array(
      'disabled' => TRUE,
    ),
  ));

  // Attach password strength check JS.
  $element['#attached']['css'][] = drupal_get_path('module', 'password_strength') . '/css/password.css';
  $element['#attached']['js'][] = array(
    'data' => array(
      'passwordStrength' => $js_settings,
    ),
    'type' => 'setting',
  );
  $element['#attached']['js'][] = array(
    'data' => drupal_get_path('module', 'password_strength') . '/js/password.js',
    'weight' => 10,
  );
}

/**
 * Validation handler for password field.
 */
function password_strength_form_password_validate($form, &$form_state) {

  // Return if new password field isn't set.
  if (empty($form_state['values']['pass'])) {
    return;
  }
  list($account, $strength) = _password_strength_calculate_strength($form, $form_state);
  $required_score = password_strength_required_score($account);

  // Only validate if score is required to be above 0.
  if ($required_score && $strength['score'] < $required_score) {
    form_set_error('pass', t("Password does not meet required strength."));
  }
  elseif (!empty($strength['matches'])) {

    // @todo don't error if same password policy rules are in place
    foreach ($strength['matches'] as $match) {
      if ($match['pattern'] == 'name') {
        form_set_error('pass', t("Password cannot match your account name"));
        break;
      }
      elseif ($match['pattern'] == 'mail') {
        form_set_error('pass', t("Password cannot match your account email address"));
        break;
      }
    }
  }
}

/**
 * Submission handler for password field.
 */
function password_strength_form_password_submit($form, &$form_state) {

  // Return if new password field isn't set.
  if (empty($form_state['values']['pass'])) {
    return;
  }
  list($account, $strength) = _password_strength_calculate_strength($form, $form_state);

  // Password has passed validation. Save strength and report change.
  // Invoke password_strength_change hook.
  module_invoke_all('password_strength_change', $account, $strength);
}

/**
 * Internal helper gets pass and account from form and calls strength check.
 *
 * @param array $form
 *   A form array for analysis.
 * @param array $form_state
 *   A form_state array for analysis.
 *
 * @return array
 *   The $account and password $strength.
 */
function _password_strength_calculate_strength($form, &$form_state) {
  global $user;
  if (isset($form['#user'])) {
    $account = $form["#user"];
  }
  else {
    $account = $user;
  }
  $pass = $form_state['values']['pass'];

  // In the case that a user is being created then neither global nor
  // $form['#user'] have good data. Try to find it in the form.
  if (!isset($account->name) && array_key_exists('name', $form_state['values'])) {
    $account->name = $form_state['values']['name'];
  }
  if (!isset($account->mail) && array_key_exists('mail', $form_state['values'])) {
    $account->mail = $form_state['values']['mail'];
  }
  $strength = password_strength_strength($pass, $account);
  return array(
    $account,
    $strength,
  );
}

/**
 * Menu callback for AJAX password check.
 */
function password_strength_ajax_check() {

  // Prevent this page from being cached.
  drupal_page_is_cacheable(FALSE);

  // Ensure we have the required data.
  if (!isset($_POST['token']) || !isset($_POST['uid']) || !isset($_POST['password']) || !is_numeric($_POST['uid'])) {
    drupal_json_output(FALSE);
    return;
  }
  $password = urldecode($_POST['password']);

  // Disallow POSTs larger than 256 characters as minor protection against DOS.
  if (strlen($password) > 256) {
    drupal_json_output(FALSE);
    return;
  }

  // Provide account as context for password strength.
  $account = user_load($_POST['uid']);
  $key = 'password_strength';
  $key .= ':' . $account->uid;

  // Validate token.
  if ($account->uid && !drupal_valid_token($_POST['token'], $key)) {
    drupal_json_output(FALSE);
    return;
  }

  // Get strength information from the password checker library.
  $strength = password_strength_strength($password, $account);

  // Get messages.
  $message_strength = password_strength_get_message_strength($strength);
  $message_requirements = password_strength_get_message_requirements($strength);
  $message_flaws = password_strength_get_message_flaws($strength);

  // Here or password_strength_strength() may need to do zxcvbn() format
  // manipulation @todo
  $data = array(
    'entropy' => $strength['entropy'],
    'matches' => $strength['matches'],
    'score' => $strength['score'],
    'score_required' => $strength['score_required'],
    'percent' => $strength['percent'],
    'message_strength' => drupal_render($message_strength),
    'message_requirements' => drupal_render($message_requirements),
    'message_flaws' => drupal_render($message_flaws),
  );
  drupal_json_output($data);
}

/**
 * Gets Zxcvbn entropy and score for a password.
 *
 * @param string $password
 *   Plain-text password to be measured.
 *
 * @param object $account
 *   Optional Drupal user account for additional contexts.
 *
 * @return array
 *   Result array with keys:
 *     entropy - float
 *     score - int
 *     match_sequence - Array of Match objects from ZxcvbnPhp
 *     matches - Array with arrays of pattern data deduced from match_sequence
 *     percent - float
 */
function password_strength_strength($password, $account = NULL) {
  global $user;
  if (empty($account)) {
    $account = $user;
  }

  // Get the required score needed for this account.
  $score_required = password_strength_required_score($account);

  // Return early if password matches email or account name.
  $strength = array(
    'entropy' => 0,
    'score' => 0,
    'score_required' => $score_required,
    'percent' => 0,
    'match_sequence' => array(),
    'matches' => array(),
  );

  // Add a length matcher to add a message if the password
  // is less then 7 characters.
  if (strlen($password) < (int) variable_get('password_strength_default_password_length', 7)) {
    $strength['matches'][] = array(
      'pattern' => 'length',
      'matched' => $password,
    );
    return $strength;
  }
  if (strtolower(trim(urldecode($password))) == $account->mail) {
    $strength['matches'][] = array(
      'pattern' => 'mail',
      'matched' => $password,
    );
    return $strength;
  }
  if (strtolower(trim(urldecode($password))) == $account->name) {
    $strength['matches'][] = array(
      'pattern' => 'name',
      'matched' => $password,
    );
    return $strength;
  }

  // Get password strength information from Zxcvbn.
  $zxcvbn = new ZxcvbnPhp\Zxcvbn();
  $strength = $zxcvbn
    ->passwordStrength($password);
  $strength['score_required'] = $score_required;
  $strength['matches'] = array();

  // Determine whether score reaches the requirements.
  if ($strength['score'] < $score_required) {
    $strength['matches'][] = array(
      'pattern' => 'score',
      'matched' => $password,
    );
  }

  // Determine which match sequences we can use to help the user
  // on the front end when choosing a password. Filter out some
  // of the stuff that wouldn't really make sense to the user.
  foreach ($strength['match_sequence'] as $match) {

    // Ignore patterns whose tokens are less then 3 characters,
    // they're not very useful to show the user.
    if (strlen($match->token) < 3) {
      continue;
    }
    $strength['matches'][] = array(
      'pattern' => password_strength_strength_pattern($match),
      'matched' => $match->token,
    );
  }

  // Calculate a percentage of the score based on the required score. This will
  // be used to animate a strength bar on the front end. Make sure this value is
  // between 0 and 100.
  $strength['percent'] = round($strength['score'] / 4 * 100);
  $strength['percent'] = $strength['percent'] >= 0 ? $strength['percent'] : 0;
  $strength['percent'] = $strength['percent'] <= 100 ? $strength['percent'] : 100;

  // Pass user account properties into checker.
  return $strength;
}

/**
 * Determines the flaw pattern from the match object.
 *
 * This is an internal key that the module will use when displaying
 * the flaws found in the password.
 *
 * @param object $match
 *   An match object.
 *
 * @return string
 *   An internal pattern identifier.
 */
function password_strength_strength_pattern($match) {
  if (isset($match->l33t) && $match->l33t) {
    $pattern = 'leetspeak';
  }
  else {
    $pattern = $match->pattern;
  }
  return $pattern;
}

/**
 * Returns a human readable list of score levels.
 *
 * @return array
 *   A list of human readable score levels.
 *
 */
function password_strength_score_list() {
  return array(
    PASSWORD_STRENGTH_SCORE_VERYWEAK => t('very weak'),
    PASSWORD_STRENGTH_SCORE_WEAK => t('weak'),
    PASSWORD_STRENGTH_SCORE_GOOD => t('good'),
    PASSWORD_STRENGTH_SCORE_STRONG => t('strong'),
    PASSWORD_STRENGTH_SCORE_VERYSTRONG => t('very strong'),
  );
}

/**
 * Returns a human readable version of the password score.
 *
 * @param int $score
 *   An integer corresponding to a password strength.
 *
 * @return string
 *   A human readable version of the score.
 */
function password_strength_get_score($score) {
  $scores = password_strength_score_list();
  return isset($scores[$score]) ? $scores[$score] : t('unknown');
}

/**
 * Returns helper strength indicator for password strength.
 *
 * @param array $strength
 *   An array containing password strength details.
 *
 * @return array
 *   A render array containing password strength markup.
 */
function password_strength_get_message_strength($strength) {
  $score = password_strength_get_score($strength['score']);

  // Show a check mark if the score reaches the required strength.
  $requirement_set = $strength['score_required'] > 0;
  $requirement_met = $strength['score'] >= $strength['score_required'];
  if ($requirement_set && $requirement_met) {
    $result_class = ' check-mark';
  }
  else {
    $result_class = '';
  }
  $build = array();
  $build['content'] = array(
    '#markup' => '<div class="name">' . t('Password strength:') . '</div> ' . '<div class="value text-score-' . $strength['score'] . $result_class . '">' . $score . '</div>',
  );
  return $build;
}

/**
 * Returns helper message for password requirements.
 *
 * @param array $strength
 *   An array containing password strength details.
 *
 * @return array
 *   A render array containing password requirements markup.
 */
function password_strength_get_message_requirements($strength) {
  $score_required = password_strength_get_score($strength['score_required']);
  $build = array();
  if ($strength['score_required'] > 0) {
    $build['content'] = array(
      '#markup' => '<div>' . t('The required minimum strength is ') . '<span class="value text-score-' . $strength['score_required'] . '">' . $score_required . '.</span></div>',
    );
  }
  return $build;
}

/**
 * Returns helper message for password strength.
 *
 * @param array $strength
 *   An array containing password strength details.
 *
 * @return array
 *   A render array.
 *
 */
function password_strength_get_message_flaws($strength) {
  $flaws = password_strength_get_flaws($strength);
  $build = array();
  if (!empty($flaws)) {
    $build['intro'] = array(
      '#markup' => '<p>' . t('The following issues were detected with your password:') . '</p>',
    );
    $build['flaws'] = array(
      '#theme' => 'item_list',
      '#items' => $flaws,
    );
  }
  return $build;
}

/**
 * Returns an array of flaws found in the password strength.
 *
 * @param array $strength
 *   An array containing password strength details.
 *
 * @return array
 *   Array of arrays suitable for theme_item_list.
 */
function password_strength_get_flaws($strength) {
  $use_eg = TRUE;
  $use_eg_live = FALSE;
  $use_eg_live_attr = FALSE;
  $flaws = array(
    'length' => array(
      'text' => t('Is shorter than @count characters', array(
        '@count' => (int) variable_get('password_strength_default_password_length', 7),
      )),
      'examples' => NULL,
    ),
    'mail' => array(
      'text' => t('Matches your email address'),
      'examples' => NULL,
    ),
    'name' => array(
      'text' => t('Matches your account name'),
      'examples' => NULL,
    ),
    'score' => array(
      'text' => t('Is not strong enough'),
      'examples' => NULL,
    ),
    'dictionary' => array(
      'text' => t('Contains dictionary words'),
      'examples' => array(
        'password',
      ),
    ),
    'sequence' => array(
      'text' => t('Has a common character sequence'),
      'examples' => array(
        '12345',
        'abc',
      ),
    ),
    'repeat' => array(
      'text' => t('Includes repeated characters'),
      'examples' => array(
        'aaa',
        '55555',
      ),
    ),
    'leetspeak' => array(
      'text' => t('Has leet (or “1337”), also known as eleet or leetspeak'),
      'examples' => array(
        'p4ssw0rd',
      ),
    ),
    'spatial' => array(
      'text' => t('Has a keyboard sequence'),
      'examples' => array(
        'qwerty',
        'asdf',
      ),
    ),
    'digit' => array(
      'text' => t('Has a series of just digits'),
      'examples' => array(
        '929',
      ),
    ),
    'date' => array(
      'text' => t('Includes a date'),
      'examples' => array(
        '19-11-1978',
      ),
    ),
    'year' => array(
      'text' => t('Includes a year'),
      'examples' => array(
        '2013',
      ),
    ),
  );

  // Collect all matched patterns.
  $matches = array();
  foreach ($strength['matches'] as $match) {
    $matches[$match['pattern']][] = $match['matched'];
  }

  // Build a list of flaws for this password.
  $items = array();
  foreach ($matches as $pattern => $match) {
    if (!isset($flaws[$pattern])) {
      continue;
    }
    $flaw = $flaws[$pattern];
    $item = array();
    $item['data'] = $flaw['text'];

    // Use default examples.
    if ($use_eg && !empty($flaw['text']) && !empty($flaw['examples'])) {
      $item['data'] .= t(' (e.g. "@match")', array(
        '@match' => implode('", "', $flaw['examples']),
      ));
    }

    // Use live examples.
    if ($use_eg_live) {
      $item['data'] .= t(' (e.g. "@match")', array(
        '@match' => implode('", "', $match),
      ));
    }

    // Use live examples in title attributes.
    if ($use_eg_live_attr) {
      $hint = t('e.g. @match', array(
        '@match' => implode(', ', $match),
      ));
      $item['data'] .= ' (<span class="hint" title="' . $hint . '">?</span>)';
    }
    $items[] = $item;
  }
  return $items;
}

/**
 * Get the password strength score required for an account.
 *
 * @param object $account
 *   Drupal user account.
 *
 * @return int
 *   Minimum password score required.
 */
function password_strength_required_score($account) {

  // Determine what is the default minimum score for account.
  $score = variable_get('password_strength_default_required_score', 0);

  // Allow other modules to alter this value (e.g. based on role).
  drupal_alter('password_strength_minimum_score', $score, $account);
  return $score;
}

Functions

Namesort descending Description
password_strength_ajax_check Menu callback for AJAX password check.
password_strength_check_strength Determine if strength should be checked.
password_strength_element_info_alter Implements hook_element_info_alter().
password_strength_form_alter Implements hook_form_alter().
password_strength_form_password_add Form extensions for password strength controls.
password_strength_form_password_js_attach Attach JS password check to a form element.
password_strength_form_password_submit Submission handler for password field.
password_strength_form_password_validate Validation handler for password field.
password_strength_get_flaws Returns an array of flaws found in the password strength.
password_strength_get_message_flaws Returns helper message for password strength.
password_strength_get_message_requirements Returns helper message for password requirements.
password_strength_get_message_strength Returns helper strength indicator for password strength.
password_strength_get_score Returns a human readable version of the password score.
password_strength_menu Implements hook_menu().
password_strength_required_score Get the password strength score required for an account.
password_strength_score_list Returns a human readable list of score levels.
password_strength_strength Gets Zxcvbn entropy and score for a password.
password_strength_strength_pattern Determines the flaw pattern from the match object.
password_strength_xautoload Implements hook_xautoload().
_password_strength_calculate_strength Internal helper gets pass and account from form and calls strength check.

Constants