tfa.module in Two-factor Authentication (TFA) 6
Same filename and directory in other branches
Two-factor authentication for Drupal.
File
tfa.moduleView 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
Name | 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. |