You are here

tfa.module in Two-factor Authentication (TFA) 6

Same filename and directory in other branches
  1. 8 tfa.module
  2. 7.2 tfa.module
  3. 7 tfa.module

Two-factor authentication for Drupal.

File

tfa.module
View source
<?php

/**
 * @file Two-factor authentication for Drupal.
 */

/**
 * Implements hook_menu().
 */
function tfa_menu() {
  $items['system/tfa/%/%'] = array(
    'title' => 'Complete authentication',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'tfa_code_form',
      2,
      3,
    ),
    'access callback' => 'tfa_entry_access',
    'access arguments' => array(
      2,
      3,
    ),
    'type' => MENU_CALLBACK,
    'file' => 'tfa.pages.inc',
  );
  $items['system/tfa-denied'] = array(
    'title' => 'TFA Denied',
    'page callback' => 'tfa_denied',
    'access callback' => 'user_is_anonymous',
    'type' => MENU_CALLBACK,
    'file' => 'tfa.pages.inc',
  );
  $items['admin/settings/tfa'] = array(
    'title' => 'Two-factor Authentication',
    'description' => 'TFA process settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'tfa_admin_settings',
    ),
    'access arguments' => array(
      'admin tfa settings',
    ),
    'file' => 'tfa.pages.inc',
  );
  return $items;
}

/**
 * Implements hook_perm().
 */
function tfa_perm() {
  return array(
    'skip tfa',
    'admin tfa settings',
  );
}

/**
 * Validate access to TFA code entry form.
 */
function tfa_entry_access($uid, $check_hash) {

  // User must be anonymous for the code entry page.
  if (!user_is_anonymous()) {
    return FALSE;
  }

  // Generate a hash for this account.
  $account = user_load(array(
    'uid' => $uid,
  ));
  $hash = tfa_login_hash($account);
  $code = tfa_get_code($uid);

  // Hash must be valid and the code must have been created within the day.
  return $hash == $check_hash && !empty($code) && $code['created'] > time() - 86400;
}

/**
 * Implements hook_cron().
 */
function tfa_cron() {

  // Remove entries from the tfa table that are older than 1 day.
  db_query('DELETE FROM {tfa} WHERE created < %d', time() - 86400);
}

/**
 * Implements hook_user().
 */
function tfa_user($op, &$edit, &$account, $category = FALSE) {
  global $user;
  if ($op == 'login') {
    if (variable_get('tfa_required', 0) && !user_access('skip tfa', $account) && !tfa_ready($account)) {
      _tfa_logout();
      drupal_goto('system/tfa-denied');
    }
    elseif (!user_access('skip tfa', $account) && tfa_ready($account)) {

      // If a code is set and not marked accepted provide TFA code process.
      $code = tfa_get_code($account->uid);
      if (!empty($code) && $code['accepted']) {

        // Code has been validated, delete and let login continue.
        tfa_delete_code($account->uid);
        drupal_goto('node');
      }
      else {

        // Hold onto UID because $user will be replaced with Anonymous.
        $uid = $user->uid;

        // Destroy the current session to halt standard authentication process.
        _tfa_logout();
        $signatory = user_load(array(
          'uid' => $uid,
        ));

        // Generate and store code.
        $code = tfa_generate_code($signatory);
        tfa_store_code($signatory->uid, $code);

        // Start send and redirection process.
        tfa_tfa_process($signatory);
      }
    }
  }
}

/**
 * Logout user. Similar to user_logout() but doesn't redirect.
 */
function _tfa_logout() {
  global $user;
  watchdog('tfa', 'Session closed for %name.', array(
    '%name' => $user->name,
  ));
  session_destroy();
  module_invoke_all('user', 'logout', NULL, $user);

  // Force anonymous user.
  $user = drupal_anonymous_user();
}

/**
 * Determine if TFA is properly configured and setup for an account.
 */
function tfa_ready($account) {
  $module = variable_get('tfa_channel', 'sms');
  $function = $module . '_tfa_api';

  // Verify channel is setup.
  if (empty($module) || !function_exists($function)) {
    return FALSE;
  }
  $channel = $function();

  // Verify there is an address (phone or other method) for this account.
  $function = $channel['address callback'];
  if (!function_exists($function)) {
    return FALSE;
  }
  $address = $function($account);
  if (empty($address)) {
    return FALSE;
  }
  return TRUE;
}

/**
 * Implements hook_help().
 */
function tfa_help($path, $arg) {
  switch ($path) {
    case 'admin/help#tfa':
      $output = '';
      $output .= '<h3>' . t('Two-factor Authentication') . '</h3>';
      $output .= '<p>' . t("TFA requires a communication channel to transfer the login code and an address to send to. By default, TFA will use the SMS Framework if installed. Enable the Profile module and add a field to store phone numbers to enable TFA.") . '<p>';
      $output .= '<p>' . t('<a href="!url">Read the TFA configuration documentation</a> on drupal.org.', array(
        '!url' => url('http://drupal.org/node/1663240'),
      )) . '</p>';
      return $output;
    case 'admin/settings/tfa':
      $output = '<p>' . t('For TFA to properly function you must select a communication channel for the TFA code. If using the SMS Framework create and select a user account field that contains the number to use. <a href="!url">Read the TFA configuration documentation</a> on drupal.org.', array(
        '!url' => url('http://drupal.org/node/1663240'),
      )) . '</p>';
      $output .= '<p>' . t('Note, unless you choose the "Require TFA process" option users will <strong>only be required to follow the TFA process</strong> if they have an address setup for code delivery.') . '</p>';
      return $output;
  }
}

/**
 * Send the code and redirect to entry form.
 */
function tfa_tfa_process($account) {

  // Send the code and if succesfull provide the entry form.
  if (!flood_is_allowed('tfa_send', variable_get('tfa_hourly_threshold', 5))) {
    drupal_set_message(t('You have reached the hourly threshold for login attempts. Please try again later.'), 'error');
    drupal_access_denied();
    exit;
  }
  if (tfa_send_code($account)) {

    // Clear any previous validation flood entries.
    db_query("DELETE FROM {flood} WHERE event = '%s' AND hostname = '%s'", 'tfa_validate', ip_address());

    // Register send event.
    flood_register_event('tfa_send');

    // Generate hash for code entry form.
    $login_hash = tfa_login_hash($account);

    // Hold onto destination and unset GET parameter.
    $query = NULL;
    if (arg(0) == 'user' && arg(1) == 'reset') {
      $query = array(
        'destination' => 'user/' . $account->uid . '/edit',
      );
    }
    if (!empty($_REQUEST['destination'])) {
      $query = array(
        'destination' => $_REQUEST['destination'],
      );
    }
    unset($_REQUEST['destination']);
    drupal_goto('system/tfa/' . $account->uid . '/' . $login_hash, $query);
  }
  else {
    drupal_set_message(t('There was an error while trying to send the login code, please try again later or contact a site administator.'));
  }
  drupal_goto('user');
}

/**
 * Generate a hash for this account for the TFA login form.
 *
 * @param object $account User account.
 * @return string Random hash.
 */
function tfa_login_hash($account) {
  $data = implode(':', array(
    $account->name,
    $account->pass,
    $account->login,
  ));
  return md5($data);
}

/**
 * Generate the code for TFA.
 *
 * @param object $account User account.
 * @return string Random code or "nonce".
 */
function tfa_generate_code($account) {
  $code_length = variable_get('tfa_code_length', 6);

  // Generate a randomized string of characters.
  $code = substr(str_shuffle(str_repeat("123456789abcdefghjkmnpqrstuvwxyz", 5)), 0, $code_length);
  return $code;
}

/**
 * Send the code to the user.
 *
 * @param object $account User account.
 * @return bool True or False if the code was sent on the secondary channel.
 */
function tfa_send_code($account) {
  $code = tfa_get_code($account->uid);
  $code = $code['code'];

  // Actual code is within element 'code'.
  $message = check_plain(variable_get('tfa_send_message', 'Login code'));

  // Variable send method, defaults to TFA method using SMS Framework.
  $module = variable_get('tfa_channel', 'sms');
  $function = $module . '_tfa_api';
  if (!empty($module) && function_exists($function)) {
    $channel = $function();
    $function = $channel['send callback'];
    $result = $function($account, $code, $message);
    return $result;
  }
  return FALSE;
}

/**
 * Store the code for state control
 *
 * @param int $uid UID of account.
 * @param string $code Code to store.
 * @return SAVED_NEW, SAVED_UPDATED or False.
 */
function tfa_store_code($uid, $code) {
  $previous_code = tfa_get_code($uid);
  $record = array(
    'uid' => $uid,
    'code' => $code,
    'accepted' => 0,
    'created' => time(),
  );
  if (!$previous_code) {
    return drupal_write_record('tfa', $record);
  }
  else {
    return drupal_write_record('tfa', $record, array(
      'uid',
    ));
  }
}

/**
 * Retreive sent code for user or FALSE if no code was set.
 *
 * @param int $uid UID of account.
 * @return array Array of with keys 'code' string and 'accepted' bool.
 */
function tfa_get_code($uid) {
  $result = db_fetch_array(db_query("SELECT code, accepted, created FROM {tfa} WHERE uid = %d", $uid));
  if (!empty($result)) {
    return $result;
  }
  return FALSE;
}

/**
 * Mark a code as accepted.
 *
 * @param int $uid UID of account.
 */
function tfa_accept_code($uid) {
  db_query("UPDATE {tfa} SET accepted = 1 WHERE uid = %d", $uid);
}

/**
 * Delete a code for a user.
 *
 * @param int $uid UID of account.
 */
function tfa_delete_code($uid) {
  db_query("DELETE FROM {tfa} WHERE uid = %d", $uid);
}

/**
 * Implements hook_tfa_api() on behalf of the SMS module.
 */
function sms_tfa_api() {
  return array(
    'title' => t('SMS Framework'),
    'send callback' => '_tfa_send_code',
    'address callback' => 'tfa_get_phone_number',
  );
}

/**
 * Address callback for SMS, uses tfa_phone_field variable.
 */
function tfa_get_phone_number($account) {
  $phone_field = variable_get('tfa_phone_field', 'profile_phone_number');
  if (empty($phone_field)) {
    return FALSE;
  }
  if (!isset($account->{$phone_field}) || empty($account->{$phone_field})) {
    return FALSE;
  }
  $phone_number = $account->{$phone_field};
  return $phone_number;
}

/**
 * Send the code using SMS Framework.
 */
function _tfa_send_code($account, $code, $message = '') {
  $phone_number = tfa_get_phone_number($account);
  $message = $message . ' ' . $code;
  return sms_send($phone_number, $message);
}

Functions

Namesort descending Description
sms_tfa_api Implements hook_tfa_api() on behalf of the SMS module.
tfa_accept_code Mark a code as accepted.
tfa_cron Implements hook_cron().
tfa_delete_code Delete a code for a user.
tfa_entry_access Validate access to TFA code entry form.
tfa_generate_code Generate the code for TFA.
tfa_get_code Retreive sent code for user or FALSE if no code was set.
tfa_get_phone_number Address callback for SMS, uses tfa_phone_field variable.
tfa_help Implements hook_help().
tfa_login_hash Generate a hash for this account for the TFA login form.
tfa_menu Implements hook_menu().
tfa_perm Implements hook_perm().
tfa_ready Determine if TFA is properly configured and setup for an account.
tfa_send_code Send the code to the user.
tfa_store_code Store the code for state control
tfa_tfa_process Send the code and redirect to entry form.
tfa_user Implements hook_user().
_tfa_logout Logout user. Similar to user_logout() but doesn't redirect.
_tfa_send_code Send the code using SMS Framework.