From 22771c54af8158aed51583363827aaabf171cd93 Mon Sep 17 00:00:00 2001 From: Prasetyo Wicaksono Date: Thu, 11 Jul 2013 14:09:06 +0700 Subject: [PATCH] Add google authenticator totp feature --- application/config/bitauth.php | 10 +- application/controllers/example.php | 76 +++- application/language/english/bitauth_lang.php | 1 + application/language/english/gauth_lang.php | 14 + application/libraries/Bitauth.php | 145 ++++++- application/libraries/GAuth.php | 354 ++++++++++++++++++ application/views/example/two_factor_auth.php | 28 ++ bitauth.sql | 28 +- 8 files changed, 621 insertions(+), 35 deletions(-) create mode 100644 application/language/english/gauth_lang.php create mode 100644 application/libraries/GAuth.php create mode 100644 application/views/example/two_factor_auth.php diff --git a/application/config/bitauth.php b/application/config/bitauth.php index 1f26e53..e19a951 100644 --- a/application/config/bitauth.php +++ b/application/config/bitauth.php @@ -169,6 +169,10 @@ * Add as many roles here as you like. * Follow the format: * 'role_slug' => 'Role Description', - */ - -); \ No newline at end of file + */ +); + + /** + * GAuth window of opportunity when entering code + */ + $config['gauth_range'] = 20; \ No newline at end of file diff --git a/application/controllers/example.php b/application/controllers/example.php index d86c1ad..39b878b 100644 --- a/application/controllers/example.php +++ b/application/controllers/example.php @@ -54,6 +54,24 @@ public function login() { $data = array(); + if($this->bitauth->login_from_token()) + { + // Check if it passed two factor auth + if($this->bitauth->logged_in()) + { + // Not require two factor auth + if($redir = $this->session->userdata('redir')) + { + $this->session->unset_userdata('redir'); + } + + redirect($redir ? $redir : 'example'); + } else { + // Redirect to enter challange code + redirect('example/twofactorauth'); + } + } + if($this->input->post()) { $this->form_validation->set_rules('username', 'Username', 'trim|required'); @@ -65,13 +83,21 @@ public function login() // Login if($this->bitauth->login($this->input->post('username'), $this->input->post('password'), $this->input->post('remember_me'))) { - // Redirect - if($redir = $this->session->userdata('redir')) + // Check if it passed two factor auth + if($this->bitauth->logged_in()) { - $this->session->unset_userdata('redir'); + // Not require two factor auth + if($redir = $this->session->userdata('redir')) + { + $this->session->unset_userdata('redir'); + } + + redirect($redir ? $redir : 'example'); + } else { + // Redirect to enter challange code + redirect('example/twofactorauth'); } - - redirect($redir ? $redir : 'example'); + } else { @@ -192,6 +218,7 @@ public function edit_user($user_id) $this->form_validation->set_rules('email', 'Email', 'trim|required|valid_email'); $this->form_validation->set_rules('active', 'Active', ''); $this->form_validation->set_rules('enabled', 'Enabled', ''); + $this->form_validation->set_rules('google_auth', 'Two Factor Auth', ''); $this->form_validation->set_rules('password_never_expires', 'Password Never Expires', ''); $this->form_validation->set_rules('groups[]', 'Groups', ''); @@ -358,4 +385,43 @@ public function logout() redirect('example'); } + /** + * Example::twofactorauth() + * + */ + public function twofactorauth() + { + $data = array(); + + if($this->bitauth->logged_in()) + { + redirect('example'); + } + + if($this->input->post()) + { + $this->form_validation->set_rules('token', 'Token', 'required'); + + if($this->form_validation->run() === TRUE) + { + if($this->bitauth->validate_gauth_code($this->input->post('token'),$this->session->userdata($this->bitauth->_cookie_elem_prefix.'user_id'))) + { + if($redir = $this->session->userdata('redir')) + { + $this->session->unset_userdata('redir'); + } + + redirect($redir ? $redir : 'example'); + } else + { + $data['error'] = $this->bitauth->get_error(); + } + } else { + $data['error'] = validation_errors(); + } + } + + $this->load->view('example/two_factor_auth', $data); + } + } \ No newline at end of file diff --git a/application/language/english/bitauth_lang.php b/application/language/english/bitauth_lang.php index a0c9dba..6cb85f8 100644 --- a/application/language/english/bitauth_lang.php +++ b/application/language/english/bitauth_lang.php @@ -62,3 +62,4 @@ $lang['bitauth_edit_group_failed'] = 'Updating group failed, please notify an administrator.'; $lang['bitauth_del_group_failed'] = 'Deleting group failed, please notify an administrator.'; $lang['bitauth_lang_not_found'] = '(No language entry for "%s" found!)'; +$lang['bitauth_invalid_token'] = 'Invalid token!'; diff --git a/application/language/english/gauth_lang.php b/application/language/english/gauth_lang.php new file mode 100644 index 0000000..df92961 --- /dev/null +++ b/application/language/english/gauth_lang.php @@ -0,0 +1,14 @@ +_date_format = $this->config->item('date_format', 'bitauth'); $this->_all_roles = $this->config->item('roles', 'bitauth'); - // Grab the first role on the list as the administrator role $slugs = array_keys($this->_all_roles); $this->_admin_role = $slugs[0]; @@ -77,6 +79,9 @@ public function __construct() // Specify any extra login fields $this->_login_fields = array(); + // Set GAuth time range on allowed codes + $this->_time_range = $this->config->item('gauth_range', 'bitauth'); + // If we're logged in, grab session values. If not, check for a "remember me" cookie if($this->logged_in()) { @@ -140,27 +145,33 @@ public function login($username, $password, $remember = FALSE, $extra = array(), $this->set_session_values($user); - if($remember != FALSE) + if($remember !== FALSE) { $this->update_remember_token($user->username, $user->user_id); } - $data = array( - 'last_login' => $this->timestamp(), - 'last_login_ip' => ip2long($_SERVER['REMOTE_ADDR']) - ); - // If user logged in, they must have remembered their password. if( ! empty($user->forgot_code)) { $data['forgot_code'] = ''; } - // Update last login timestamp and IP - $this->update_user($user->user_id, $data); + if($user->google_auth) + { + return TRUE; + } else { - $this->log_attempt($user->user_id, TRUE); - return TRUE; + $data = array( + 'last_login' => $this->timestamp(), + 'last_login_ip' => ip2long($_SERVER['REMOTE_ADDR']) + ); + + // Update last login timestamp and IP + $this->update_user($user->user_id, $data); + + $this->log_attempt($user->user_id, TRUE); + return TRUE; + } } $this->log_attempt($user->user_id, FALSE); @@ -403,11 +414,15 @@ public function get_session_values() */ public function update_remember_token($username = NULL, $user_id = NULL) { - if( ! $this->logged_in()) + if( ! $this->session->userdata($this->_cookie_elem_prefix.'google_auth')) { - return; + if( ! $this->logged_in()) + { + return; + } } + if($username === NULL) { $username = $this->username; @@ -524,6 +539,12 @@ public function add_user($data, $require_activation = NULL) $data['password'] = $this->hash_password($data['password']); $data['password_last_set'] = $this->timestamp(); + if(isset($userdata['google_auth'])) + { + $data['google_auth'] = 1; + unset($userdata['google_auth']); + $data['google_key'] = $this->generate_gauth_key(); + } $this->db->trans_begin(); @@ -700,6 +721,22 @@ public function update_user($id, $data) } } + if(isset($userdata['google_auth'])) + { + if($userdata['google_auth'] == 1) + { + $data['google_auth'] = TRUE; + $data['google_key'] = $this->generate_gauth_key(); + unset($userdata['google_auth']); + } else if ($userdata['google_auth'] == 0) + { + $data['google_auth'] = FALSE; + $data['google_key'] = ''; + unset($userdata['google_auth']); + } + } + + if( ! empty($data['password'])) { $new_password = $this->hash_password($data['password']); @@ -1348,6 +1385,77 @@ public function get_group_by_id($id) return FALSE; } + /** + * Bitauth::set_gauth_key() + * + * Set Google Authenticator secret key for user + * + */ + public function set_gauth_key($user_id) + { + + } + + /** + * Bitauth::set_gauth_key() + * + * Get Google Authenticator secret key by user id + * + */ + public function get_gauth_key($user_id) + { + $query = $this->db->select('google_key') + ->from('bitauth_users') + ->where('user_id',$user_id) + ->get(); + + $result = $query->row_array(); + return $result['google_key']; + } + + /** + * Bitauth::generate_gauth_key() + * + * Generate Google Authenticator secret key + * + */ + public function generate_gauth_key() + { + return $this->gauth->generateCode(); + } + + /** + * Bitauth::generate_gauth_key() + * + * Validate Google Authenticator code + * + */ + public function validate_gauth_code($code = '', $user_id = '') + { + $this->gauth->setRange($this->_time_range); + $this->gauth->setInitKey($this->get_gauth_key($user_id)); + if ($this->gauth->validateCode($code)) + { + $data = array( + 'last_login' => $this->timestamp(), + 'last_login_ip' => ip2long($_SERVER['REMOTE_ADDR']) + ); + + // Update last login timestamp and IP + $this->update_user($this->session->userdata($this->_cookie_elem_prefix.'user_id'),$data); + + $this->log_attempt($this->session->userdata($this->_cookie_elem_prefix.'user_id'), TRUE); + + $this->session->set_userdata($this->_cookie_elem_prefix.'google_code',$code); + return TRUE; + } else + { + $this->set_error($this->lang->line('bitauth_invalid_token')); + $this->log_attempt($this->session->userdata($this->_cookie_elem_prefix.'user_id'), FALSE); + return FALSE; + } + } + /** * Bitauth::set_error() * @@ -1416,7 +1524,12 @@ public function generate_code() */ public function logged_in() { - return (bool)$this->session->userdata($this->_cookie_elem_prefix.'username'); + if($this->session->userdata($this->_cookie_elem_prefix.'google_auth')) + { + return (bool)$this->session->userdata($this->_cookie_elem_prefix.'username') && (bool)$this->session->userdata($this->_cookie_elem_prefix.'google_code'); + } else { + return (bool)$this->session->userdata($this->_cookie_elem_prefix.'username'); + } } /** @@ -1585,6 +1698,10 @@ public function _assign_libraries() )); $this->phpass = $CI->phpass; + // Load GAuth library + $CI->load->library('GAuth'); + $this->gauth = $CI->gauth; + return; } diff --git a/application/libraries/GAuth.php b/application/libraries/GAuth.php new file mode 100644 index 0000000..d789389 --- /dev/null +++ b/application/libraries/GAuth.php @@ -0,0 +1,354 @@ + + * @package GAuth + * @license MIT + */ + +class GAuth +{ + /** + * Internal lookup table + * @var array + */ + private $lookup = array(); + + /** + * Initialization key + * @var string + */ + private $initKey = null; + + /** + * Seconds between key refreshes + * @var integer + */ + private $refreshSeconds = 30; + + /** + * Length of codes to generate + * @var integer + */ + private $codeLength = 6; + + /** + * Range plus/minus for "window of opportunity" on allowed codes + * @var integer + */ + private $range = 2; + + /** + * Initialize the object and set up the lookup table + * Optionally the Initialization key + * + * @param string $initKey Initialization key + */ + public function __construct($initKey = null) + { + $this->_assign_libraries(); + $this->buildLookup(); + + if ($initKey !== null) { + $this->setInitKey($initKey); + } + } + + /** + * Build the base32 lookup table + * + * @return null + */ + public function buildLookup() + { + $lookup = array_combine( + array_merge(range('A', 'Z'), range(2, 7)), + range(0, 31) + ); + $this->setLookup($lookup); + } + + /** + * Get the current "range" value + * @return integer Range value + */ + public function getRange() + { + return $this->range; + } + + /** + * Set the "range" value + * + * @param integer $range Range value + * @return \GAuth\Auth instance + */ + public function setRange($range) + { + if (!is_numeric($range)) { + log_message('error', $this->lang->line('gauth_invalid_range')); + show_error($this->lang->line('gauth_invalid_range')); + } + $this->range = $range; + return $this; + } + + /** + * Set the initialization key for the object + * + * @param string $key Initialization key + * @throws \InvalidArgumentException If hash is not valid base32 + * @return \GAuth\Auth instance + */ + public function setInitKey($key) + { + if (preg_match('/^['.implode('', array_keys($this->getLookup())).']+$/', $key) == false) { + log_message('error', $this->lang->line('gauth_invalid_range')); + show_error($this->lang->line('gauth_invalid_range')); + } + $this->initKey = $key; + return $this; + } + + /** + * Get the current Initialization key + * + * @return string Initialization key + */ + public function getInitKey() + { + return $this->initKey; + } + + /** + * Set the contents of the internal lookup table + * + * @param array $lookup Lookup data set + * @throws \InvalidArgumentException If lookup given is not an array + * @return \GAuth\Auth instance + */ + public function setLookup($lookup) + { + if (!is_array($lookup)) { + throw new \InvalidArgumentException('Lookup value must be an array'); + } + $this->lookup = $lookup; + return $this; + } + + /** + * Get the current lookup data set + * + * @return array Lookup data + */ + public function getLookup() + { + return $this->lookup; + } + + /** + * Get the number of seconds for code refresh currently set + * + * @return integer Refresh in seconds + */ + public function getRefresh() + { + return $this->refreshSeconds; + } + + /** + * Set the number of seconds to refresh codes + * + * @param integer $seconds Seconds to refresh + * @throws \InvalidArgumentException If seconds value is not numeric + * @return \GAuth\Auth instance + */ + public function setRefresh($seconds) + { + if (!is_numeric($seconds)) { + log_message('error', $this->lang->line('gauth_invalid_refresh')); + show_error($this->lang->line('gauth_invalid_refresh')); + } + $this->refreshSeconds = $seconds; + return $this; + } + + /** + * Get the current length for generated codes + * + * @return integer Code length + */ + public function getCodeLength() + { + return $this->codeLength; + } + + /** + * Set the length of the generated codes + * + * @param integer $length Code length + * @return \GAuth\Auth instance + */ + public function setCodeLength($length) + { + $this->codeLength = $length; + return $this; + } + + /** + * Validate the given code + * + * @param string $code Code entered by user + * @param string $initKey Initialization key + * @param string $timestamp Timestamp for calculation + * @param integer $range Seconds before/after to validate hash against + * @throws \InvalidArgumentException If incorrect code length + * @return boolean Pass/fail of validation + */ + public function validateCode($code, $initKey = null, $timestamp = null, $range = null) + { + if (strlen($code) !== $this->getCodeLength()) { + log_message('error', $this->lang->line('gauth_invalid_code_lenght')); + show_error($this->lang->line('gauth_invalid_code_lenght')); + } + + $range = ($range == null) ? $this->getRange() : $range; + $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; + $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; + + $binary = $this->base32_decode($initKey); + + for ($time = ($timestamp - $range); $time <= ($timestamp + $range); $time++) { + if ($this->generateOneTime($binary, $time) == $code) { + return true; + } + } + return false; + } + + /** + * Generate a one-time code + * + * @param string $initKey Initialization key [optional] + * @param string $timestamp Timestamp for calculation [optional] + * @return string Geneerated code/hash + */ + public function generateOneTime($initKey = null, $timestamp = null) + { + $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; + $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; + + $hash = hash_hmac ( + 'sha1', + pack('N*', 0) . pack('N*', $timestamp), + $initKey, + true + ); + + return str_pad($this->truncateHash($hash), $this->getCodeLength(), '0', STR_PAD_LEFT); + } + + /** + * Generate a code/hash + * Useful for making Initialization codes + * + * @param integer $length Length for the generated code + * @return string Generated code + */ + public function generateCode($length = 16) + { + $lookup = implode('', array_keys($this->getLookup())); + $code = ''; + + for ($i = 0; $i < $length; $i++) { + $code .= $lookup[mt_rand(0, strlen($lookup)-1)]; + } + + return $code; + } + + /** + * Geenrate the timestamp for the calculation + * + * @return integer Timestamp + */ + public function generateTimestamp() + { + return floor(microtime(true)/$this->getRefresh()); + } + + /** + * Truncate the given hash down to just what we need + * + * @param string $hash Hash to truncate + * @return string Truncated hash value + */ + public function truncateHash($hash) + { + $offset = ord($hash[19]) & 0xf; + + return ( + ((ord($hash[$offset+0]) & 0x7f) << 24 ) | + ((ord($hash[$offset+1]) & 0xff) << 16 ) | + ((ord($hash[$offset+2]) & 0xff) << 8 ) | + (ord($hash[$offset+3]) & 0xff) + ) % pow(10, $this->getCodeLength()); + } + + /** + * Base32 decoding function + * + * @param string base32 encoded hash + * @throws \InvalidArgumentException When hash is not valid + * @return string Binary value of hash + */ + public function base32_decode($hash) + { + $lookup = $this->getLookup(); + + if (preg_match('/^['.implode('', array_keys($lookup)).']+$/', $hash) == false) { + log_message('error', $this->lang->line('gauth_invalid_base32')); + show_error($this->lang->line('gauth_invalid_base32')); + } + + $hash = strtoupper($hash); + $buffer = 0; + $length = 0; + $binary = ''; + + for ($i = 0; $i < strlen($hash); $i++) { + $buffer = $buffer << 5; + $buffer += $lookup[$hash[$i]]; + $length += 5; + + if ($length >= 8) { + $length -= 8; + $binary .= chr(($buffer & (0xFF << $length)) >> $length); + } + } + + return $binary; + } + + public function _assign_libraries() + { + if($CI =& get_instance()) + { + $this->load = $CI->load; + $this->lang = $CI->lang; + + $this->lang->load('gauth'); + + return; + } + + log_message('error', $this->lang->line('gauth_instance_na')); + show_error($this->lang->line('gauth_instance_na')); + } +} diff --git a/application/views/example/two_factor_auth.php b/application/views/example/two_factor_auth.php new file mode 100644 index 0000000..a8619d1 --- /dev/null +++ b/application/views/example/two_factor_auth.php @@ -0,0 +1,28 @@ + + + + + BitAuth: Login + + +Two Factor Auth'; + + echo form_open(current_url()); + echo form_label('Token','token'); + echo form_input('token'); + echo form_submit('login','Login'); + echo ( ! empty($error) ? $error : '' ); + echo form_close(); + +?> + + diff --git a/bitauth.sql b/bitauth.sql index 7494526..812c505 100644 --- a/bitauth.sql +++ b/bitauth.sql @@ -1,13 +1,14 @@ -- phpMyAdmin SQL Dump --- version 3.3.9 +-- version 3.4.11.1deb1 -- http://www.phpmyadmin.net -- -- Host: localhost --- Generation Time: Aug 20, 2011 at 03:19 PM --- Server version: 5.1.53 --- PHP Version: 5.3.4 +-- Generation Time: Jul 07, 2013 at 06:20 AM +-- Server version: 5.5.31 +-- PHP Version: 5.4.6-1ubuntu1.2 SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; @@ -59,8 +60,8 @@ CREATE TABLE IF NOT EXISTS `bitauth_groups` ( -- INSERT INTO `bitauth_groups` (`group_id`, `name`, `description`, `roles`) VALUES -(1, 'Administrators', 'Administrators (Full Access)', 1), -(2, 'Users', 'Default User Group', 0); +(1, 'Administrators', 'Administrators (Full Access)', '1'), +(2, 'Users', 'Default User Group', '0'); -- -------------------------------------------------------- @@ -78,11 +79,6 @@ CREATE TABLE IF NOT EXISTS `bitauth_logins` ( KEY `user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; --- --- Dumping data for table `bitauth_logins` --- - - -- -------------------------------------------------------- -- @@ -123,6 +119,8 @@ CREATE TABLE IF NOT EXISTS `bitauth_users` ( `forgot_code` varchar(40) NOT NULL, `forgot_generated` datetime NOT NULL, `enabled` tinyint(1) NOT NULL DEFAULT '1', + `google_auth` tinyint(1) NOT NULL DEFAULT '0', + `google_key` varchar(20) NOT NULL, `last_login` datetime NOT NULL, `last_login_ip` int(10) NOT NULL, PRIMARY KEY (`user_id`) @@ -132,5 +130,9 @@ CREATE TABLE IF NOT EXISTS `bitauth_users` ( -- Dumping data for table `bitauth_users` -- -INSERT INTO `bitauth_users` (`user_id`, `username`, `password`, `password_last_set`, `password_never_expires`, `remember_me`, `activation_code`, `active`, `forgot_code`, `forgot_generated`, `enabled`, `last_login`, `last_login_ip`) VALUES -(1, 'admin', '$2a$08$560JEYl2Np/7/6RLc/mq/ecnumuBXig3e.pHh1lnH1pgpk94sTZhu', now(), 0, '', '', 1, '', '0000-00-00 00:00:00', 1, '0000-00-00 00:00:00', 0); +INSERT INTO `bitauth_users` (`user_id`, `username`, `password`, `password_last_set`, `password_never_expires`, `remember_me`, `activation_code`, `active`, `forgot_code`, `forgot_generated`, `enabled`, `google_auth`, `google_key`, `last_login`, `last_login_ip`) VALUES +(1, 'admin', '$2a$08$560JEYl2Np/7/6RLc/mq/ecnumuBXig3e.pHh1lnH1pgpk94sTZhu', '2013-07-07 06:13:56', 0, '', '', 1, '', '0000-00-00 00:00:00', 1, 0, '', '0000-00-00 00:00:00', 0); + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;