You are here

ga_login.module in Google Authenticator login 7

Same filename and directory in other branches
  1. 6 ga_login.module

Main ga_login module.

File

ga_login.module
View source
<?php

/**
 * @file
 * Main ga_login module.
 */

/**
 * Token is valid.
 */
define('GA_LOGIN_TOKEN_VALID', 'valid');

/**
 * Token is invalid.
 */
define('GA_LOGIN_TOKEN_INVALID', 'invalid');

/**
 * Token is missing. Needs to be generated.
 */
define('GA_LOGIN_TOKEN_MISSING', 'missing');

/**
 * Implements hook_menu().
 */
function ga_login_menu() {
  $items = array();
  $items['user/%user/ga_login'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'GA login',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'ga_login_create_form',
      1,
    ),
    'access callback' => 'ga_login_create_access',
    'access arguments' => array(
      1,
    ),
    'file' => 'ga_login.pages.inc',
  );
  $items['user/%user/ga_login/view'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => 'GA login',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'ga_login_create_form',
      1,
    ),
    'access callback' => 'ga_login_create_access',
    'access arguments' => array(
      1,
    ),
    'file' => 'ga_login.pages.inc',
    'weight' => 0,
  );
  $items['user/%user/ga_login/delete'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'GA login delete',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'ga_login_delete_form',
      1,
    ),
    'access callback' => 'ga_login_delete_access',
    'access arguments' => array(
      1,
    ),
    'file' => 'ga_login.pages.inc',
    'weight' => 1,
  );
  $items['admin/config/people/ga_login'] = array(
    'type' => MENU_NORMAL_ITEM,
    'title' => 'GA login',
    'description' => 'Administer Google Authenticator login settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'ga_login_admin_settings_form',
    ),
    'access arguments' => array(
      'administer ga_login settings',
    ),
    'file' => 'ga_login.admin.inc',
  );
  return $items;
}

/**
 * Access callback for creating codes.
 */
function ga_login_create_access($target_account, $account = NULL) {
  if (is_null($account)) {
    global $user;
    $account = $user;
  }
  if ($account->uid == $target_account->uid) {
    if (user_access('create own login code', $account) || user_access('create others login codes', $account)) {
      return TRUE;
    }
    elseif (user_access('create own login code once', $account)) {

      // Check if the user already has a code.
      return !_ga_login_account_has_code($account);
    }
  }
  return user_access('create others login codes', $account);
}

/**
 * Access callback for deleting codes.
 */
function ga_login_delete_access($target_account, $account = NULL) {

  // If the account does not have a code, no need to delete it.
  if (!_ga_login_account_has_code($target_account)) {
    return FALSE;
  }
  if (is_null($account)) {
    global $user;
    $account = $user;
  }
  if ($account->uid == $target_account->uid) {

    // Only allow deleting of own code if they still can login.
    if (user_access('login without code', $account) && (user_access('delete own login code', $account) || user_access('delete others login codes', $account))) {
      return TRUE;
    }
  }
  return user_access('delete others login codes', $account);
}

/**
 * Implements hook_permission().
 */
function ga_login_permission() {
  return array(
    'create own login code once' => array(
      'title' => t('Create own login code only once'),
      'description' => t('Allows users to create their own GA login code only once and deny further generations.'),
    ),
    'create own login code' => array(
      'title' => t('Create own login code'),
      'description' => t('Allows users to create their own GA login code (more than once).'),
    ),
    'delete own login code' => array(
      'title' => t('Delete own login code'),
      'description' => t('Allows users to delete their own GA login code.'),
    ),
    'create others login codes' => array(
      'title' => t("Create others' login codes"),
      'description' => t("Allows users to create others' GA login codes"),
      'restrict access' => TRUE,
    ),
    'delete others login codes' => array(
      'title' => t("Delete others' login codes"),
      'description' => t("Allows users to delete others' GA login codes"),
      'restrict access' => TRUE,
    ),
    'login without code' => array(
      'title' => t('Login without code'),
      'description' => t("With this permission, users don't have to fill in the GA login code"),
      'restrict access' => TRUE,
    ),
    'require code' => array(
      'title' => t('Require code'),
      'description' => t('With this permission, users are required to fill in the GA login code. Trumps "login without code".'),
      'restrict access' => TRUE,
    ),
    'administer ga_login settings' => array(
      'title' => t('Administer GA login settings'),
      'description' => t('Administer Google Authenticator login settings'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Check if the given account wants to be forced to use tfa.
 */
function _ga_login_force_tfa($account) {
  $cant_make = !user_access('create own login code once', $account) && !user_access('create own login code', $account);

  // If the user can't create their own code and they don't already have one,
  // then we shouldn't force them to use it.
  if ($cant_make && !_ga_login_account_has_code($account)) {
    return FALSE;
  }

  // If the user belongs to any role that is required to use the code,
  // it is required, unless uid 1 and we don't require uid 1 to use a code.
  if (user_access('require code', $account) && ($account->uid != 1 || variable_get('ga_login_always_for_uid1', 0))) {
    return TRUE;
  }
  if (user_access('login without code', $account)) {
    return isset($account->data['ga_login_force_tfa']) ? $account->data['ga_login_force_tfa'] : FALSE;
  }
  return TRUE;
}

/**
 * Check if the given account does have a code.
 */
function _ga_login_account_has_code($account) {
  $ga = _ga_login_get_class();
  $username = _ga_login_username($account);
  return $ga
    ->hasToken($username);
}

/**
 * Returns the GALoginGA class.
 */
function _ga_login_get_class() {
  module_load_include('php', 'ga_login', 'ga_login.class');
  return new GALoginGA(variable_get('ga_login_totp_skew', 10), variable_get('ga_login_hotp_skew', 10));
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add GA Login Code field to user login form.
 */
function ga_login_form_user_login_alter(&$form, &$form_state, $form_id) {
  $form['gacode'] = array(
    '#type' => 'gacode',
    '#title' => t('Code'),
    '#required' => FALSE,
  );
  $form['name']['#weight'] = 1;
  $form['pass']['#weight'] = 2;
  $form['gacode']['#weight'] = 3;
  $form['submit']['#weight'] = 4;
  if (isset($form['links'])) {
    $form['links']['#weight'] = 5;
  }

  // Normalize keys to start from 0.
  $form['#validate'] = array_values($form['#validate']);
  $validate_before = array_slice($form['#validate'], 0, array_search('user_login_final_validate', $form['#validate']));
  $validate_after = array_slice($form['#validate'], array_search('user_login_final_validate', $form['#validate']));

  // Insert our validation function directly before user_login_final_validate.
  $form['#validate'] = array_merge($validate_before, array(
    'ga_login_user_login_validate',
  ), $validate_after);

  // Add submit handler to conditionally redirect the user to create
  // a new GA login code.
  $form['#submit'][] = 'ga_login_user_login_submit_code_needed';
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add GA Login Code field to user login block.
 */
function ga_login_form_user_login_block_alter(&$form, &$form_state, $form_id) {
  ga_login_form_user_login_alter($form, $form_state, $form_id);
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add checkbox to enable two factor authentication.
 */
function ga_login_form_user_profile_form_alter(&$form, &$form_state, $form_id) {
  $account = $form['#user'];
  $register = $account->uid > 0 ? FALSE : TRUE;

  // Add some more settings to the user profile form.
  $form['ga_login'] = array(
    '#type' => 'fieldset',
    '#title' => t('Two factor authentication'),
    '#weight' => 1,
    '#access' => !$register && user_access('login without code', $account),
  );
  $form['ga_login']['ga_login_force_tfa'] = array(
    '#type' => 'checkbox',
    '#title' => t('Protect my account with two-factor-authentication'),
    '#default_value' => isset($account->data['ga_login_force_tfa']) ? $account->data['ga_login_force_tfa'] : FALSE,
    '#description' => t('Check this box to force two-factor-authentication during login. If you decide to do so and haven\'t yet created your key, then please also refer to <a href="@url">GA Login</a>.', array(
      '@url' => url('user/' . $account->uid . '/ga_login'),
    )),
  );
  if (ga_login_delete_access($account)) {
    $form['ga_login']['ga_login_delete_code'] = array(
      '#type' => 'submit',
      '#value' => t('Delete GA Login Code'),
      '#submit' => array(
        'ga_login_delete_code_confirm_redirect',
      ),
    );
  }
}

/**
 * A simple redirect to a confirmation page before deleting a code.
 */
function ga_login_delete_code_confirm_redirect($form, &$form_state) {
  drupal_goto('user/' . $form['#user']->uid . '/ga_login/delete');
}

/**
 * Implements hook_user_presave().
 *
 * If a user enables 'Protect my account with two-factor-authentication'
 * make sure he has setup a code, if not redirect to the creation page.
 */
function ga_login_user_presave(&$edit, $account, $category) {
  if (isset($edit['ga_login_force_tfa'])) {
    $edit['data']['ga_login_force_tfa'] = $edit['ga_login_force_tfa'];
    if ($edit['ga_login_force_tfa'] && empty($account->data['ga_login_force_tfa']) && !_ga_login_account_has_code($account)) {

      // If force tfa got switched on and the user has no code yet,
      // redirect to the code creation page after saving.
      $edit['data']['ga_login_force_tfa'] = FALSE;
      $_GET['destination'] = 'user/' . $account->uid . '/ga_login';
    }
  }
}

/**
 * Validate callback for login form.
 *
 * Checks if the ga_login code is needed and valid.
 *
 * @see ga_login_form_alter()
 */
function ga_login_user_login_validate($form, &$form_state) {
  $code = $form_state['values']['gacode'];
  if (!empty($form_state['uid']) && !form_get_errors()) {

    // Authentication was successful, check the GA code.
    $name = $form_state['values']['name'];
    $account = user_load_by_name($name);
    if (_ga_login_force_tfa($account) || !empty($code) || $account->uid == 1 && variable_get('ga_login_always_for_uid1', 0)) {
      $ga = _ga_login_get_class();
      $username = _ga_login_username($account);
      if ($ga
        ->hasToken($username)) {
        $keyok = $ga
          ->authenticateUser($username, $code);
        if (!$keyok) {
          $form_state['ga_code'] = GA_LOGIN_TOKEN_INVALID;

          // Clear uid so that the login fails and a flood event is registered.
          $form_state['uid'] = FALSE;
        }
        else {
          $form_state['ga_code'] = GA_LOGIN_TOKEN_VALID;
        }
      }
      else {
        $form_state['ga_code'] = GA_LOGIN_TOKEN_MISSING;
        if ($account->uid != 1 && user_access('require code', $account)) {

          // A code is required but not provided.
          form_set_error('gacode', t("Your code is required to log in."));
        }
      }
    }
    elseif ($account->uid != 1 && user_access('require code', $account)) {

      // A code is required but not provided.
      form_set_error('gacode', t("Your code is required to log in."));
    }
  }
  if (!empty($code) && (!isset($form_state['ga_code']) || $form_state['ga_code'] == GA_LOGIN_TOKEN_INVALID)) {
    form_set_error('gacode', t("Your code isn't valid or has already been used."));
  }
}

/**
 * Submit callback for login form.
 *
 * Checks if the user has to use ga_login, but doesn't yet have a code.
 *
 * @see ga_login_form_alter()
 */
function ga_login_user_login_submit_code_needed($form, &$form_state) {
  $name = $form_state['values']['name'];
  $code = $form_state['values']['gacode'];
  $account = user_load_by_name($name);

  // Check if user needs to generate a token.
  if (isset($form_state['ga_code']) && $form_state['ga_code'] == GA_LOGIN_TOKEN_MISSING) {

    // Make sure the user can create a code.
    if (user_access('create own login code once', $account) || user_access('create own login code', $account) || user_access('create others login codes', $account)) {
      unset($_GET['destination']);
      drupal_set_message(t("You don't have a login code yet. Please add one to your account below."), 'warning');
      $form_state['redirect'] = "user/{$account->uid}/ga_login";
    }
  }
}

/**
 * Implements hook_mobile_codes_default_mobile_codes_preset_alter().
 */
function ga_login_mobile_codes_default_mobile_codes_preset_alter(&$export) {
  $preset = new stdClass();
  $preset->disabled = FALSE;

  /* Edit this to true to make a default preset disabled initially */
  $preset->api_version = 2;
  $preset->name = 'ga_login';
  $preset->provider = 'google';
  $preset->defaults = array(
    'width' => '200',
    'height' => '200',
    'output_encoding' => 'UTF-8',
  );
  $export['ga_login'] = $preset;
}

/**
 * Create a site specific username.
 */
function _ga_login_username($account, $encode = TRUE) {
  $realm = variable_get('ga_login_textname', variable_get('site_name', 'Drupal'));
  $suffix = variable_get('ga_login_textid', '');
  $username = format_string('!account@!realm!suffix', array(
    '!account' => $account->name,
    '!realm' => $realm,
    '!suffix' => $suffix,
  ));
  return $encode ? rawurlencode($username) : $username;
}

/**
 * Removes the GA login code associated with an account.
 */
function ga_login_delete_code($account) {
  $username = _ga_login_username($account);
  $result = db_delete('ga_login')
    ->condition('name', $username)
    ->execute();
  if ($result) {
    drupal_set_message(t("Successfully deleted the GA Login code for @name", array(
      '@name' => format_username($account),
    )));

    // Disable TFA for this account, since they no longer have a code.
    user_save($account, array(
      'data' => array(
        'ga_login_force_tfa' => FALSE,
      ),
    ));
  }
  else {
    drupal_set_message(t("There was a problem deleting the GA Login code for @name", array(
      '@name' => format_username($account),
    )), 'error');
  }
}

/**
 * Implements hook_element_info().
 */
function ga_login_element_info() {
  $types['gacode'] = array(
    '#input' => TRUE,
    '#uid' => NULL,
    '#size' => 6,
    '#maxlength' => 6,
    '#autocomplete_path' => FALSE,
    '#process' => array(
      'ajax_process_form',
    ),
    '#element_validate' => array(
      'ga_login_validate_gacode',
    ),
    '#theme' => 'gacode',
    '#theme_wrappers' => array(
      'form_element',
    ),
    '#_new_ga_code' => FALSE,
  );
  return $types;
}

/**
 * Form element validation handler for gacode field.
 *
 * Note that #required is validated by _form_validate() already.
 */
function ga_login_validate_gacode(&$element, &$form_state) {
  $code = $element['#value'];
  if ($code === '') {
    return;
  }
  $name = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];

  // Ensure the input exactly 6 digits.
  if (strlen($code) != 6 || !ctype_digit($code)) {
    form_error($element, t('%name has to be exactly 6 digits.', array(
      '%name' => $name,
    )));
    return;
  }

  // Load the associated user account.
  $account = NULL;
  if (is_null($element['#uid'])) {
    global $user;

    // Make sure we have a user, if not bail out.
    if ($user->uid === 0) {
      return;
    }
    $account = user_load($user->uid);
  }
  else {
    $account = user_load($element['#uid']);
  }
  $ga = _ga_login_get_class();
  $username = _ga_login_username($account);
  if ($ga
    ->hasToken($username) && !$element['#_new_ga_code']) {
    $keyok = $ga
      ->authenticateUser($username, $code);
    if (!$keyok) {
      form_error($element, t('%name has an invalid code.', array(
        '%name' => $name,
      )));
    }
  }
}

/**
 * Implements hook_theme().
 */
function ga_login_theme() {
  return array(
    'gacode' => array(
      'arguments' => array(
        'element' => NULL,
      ),
      'render element' => 'element',
      'file' => 'ga_login.theme.inc',
    ),
  );
}

/**
 * Implements hook_user_operations().
 */
function ga_login_user_operations() {
  if (user_access('delete others login codes')) {
    $operations = array(
      'reset_ga_login' => array(
        'label' => t('Reset GA login code'),
        'callback' => 'ga_login_user_operations_reset_ga_login',
      ),
    );
  }
  return $operations;
}

/**
 * Callback function for admin bulk GA code reset.
 */
function ga_login_user_operations_reset_ga_login($accounts) {
  $accounts = user_load_multiple($accounts);
  foreach ($accounts as $account) {
    ga_login_delete_code($account);
  }
}

/**
 * Implements hook_user_delete().
 */
function ga_login_user_delete($account) {
  ga_login_delete_code($account);
}

Functions

Namesort descending Description
ga_login_create_access Access callback for creating codes.
ga_login_delete_access Access callback for deleting codes.
ga_login_delete_code Removes the GA login code associated with an account.
ga_login_delete_code_confirm_redirect A simple redirect to a confirmation page before deleting a code.
ga_login_element_info Implements hook_element_info().
ga_login_form_user_login_alter Implements hook_form_FORM_ID_alter().
ga_login_form_user_login_block_alter Implements hook_form_FORM_ID_alter().
ga_login_form_user_profile_form_alter Implements hook_form_FORM_ID_alter().
ga_login_menu Implements hook_menu().
ga_login_mobile_codes_default_mobile_codes_preset_alter Implements hook_mobile_codes_default_mobile_codes_preset_alter().
ga_login_permission Implements hook_permission().
ga_login_theme Implements hook_theme().
ga_login_user_delete Implements hook_user_delete().
ga_login_user_login_submit_code_needed Submit callback for login form.
ga_login_user_login_validate Validate callback for login form.
ga_login_user_operations Implements hook_user_operations().
ga_login_user_operations_reset_ga_login Callback function for admin bulk GA code reset.
ga_login_user_presave Implements hook_user_presave().
ga_login_validate_gacode Form element validation handler for gacode field.
_ga_login_account_has_code Check if the given account does have a code.
_ga_login_force_tfa Check if the given account wants to be forced to use tfa.
_ga_login_get_class Returns the GALoginGA class.
_ga_login_username Create a site specific username.

Constants

Namesort descending Description
GA_LOGIN_TOKEN_INVALID Token is invalid.
GA_LOGIN_TOKEN_MISSING Token is missing. Needs to be generated.
GA_LOGIN_TOKEN_VALID Token is valid.