diff --git a/application/config/autoload.php b/application/config/autoload.php new file mode 100644 index 0000000..c765694 --- /dev/null +++ b/application/config/autoload.php @@ -0,0 +1,4 @@ +_public_key = $this->config->item('recaptcha_public_key', 'bitauth'); + $this->_private_key = $this->config->item('recaptcha_private_key', 'bitauth'); + $this->load->library('bitauth'); $this->load->helper('form'); @@ -102,12 +105,30 @@ public function index() $this->load->view('example/users', array('bitauth' => $this->bitauth, 'users' => $this->bitauth->get_users())); } + public function _recaptcha_check() + { + $resp = $this->recaptcha->recaptcha_check_answer($this->_private_key, $_SERVER["REMOTE_ADDR"], $this->input->post('recaptcha_challenge_field'), $this->input->post('recaptcha_response_field')); + + if( ! $resp->is_valid) + { + $this->form_validation->set_message('_recaptcha_check', $this->lang->line('bitauth_recaptcha_error')); + return FALSE; + } + else + { + return TRUE; + } + } + /** * Example::register() * */ public function register() { + $this->load->library('Recaptcha'); + $data['recaptcha'] = $this->recaptcha->recaptcha_get_html($this->_public_key); + if($this->input->post()) { $this->form_validation->set_rules('username', 'Username', 'trim|required|bitauth_unique_username'); @@ -115,17 +136,60 @@ public function register() $this->form_validation->set_rules('email', 'Email', 'trim|required|valid_email'); $this->form_validation->set_rules('password', 'Password', 'required|bitauth_valid_password'); $this->form_validation->set_rules('password_conf', 'Password Confirmation', 'required|matches[password]'); + $this->form_validation->set_rules('recaptcha_response_field', 'Captcha code', 'required|callback__recaptcha_check'); if($this->form_validation->run() == TRUE) { - unset($_POST['submit'], $_POST['password_conf']); + unset($_POST['submit'], $_POST['password_conf'], $_POST['recaptcha_response_field'], $_POST['recaptcha_challenge_field']); $this->bitauth->add_user($this->input->post()); redirect('example/login'); } } - $this->load->view('example/add_user', array('title' => 'Register')); + $this->load->view('example/register', array('title' => 'Register')); + } + + public function forgot_password() + { + if($this->input->post()) + { + $this->form_validation->set_rules('email', 'Email', 'trim|required|valid_email'); + + if($this->form_validation->run() == TRUE) + { + $this->bitauth->generate_forgot_code($this->form_validation->set_value('email')); + redirect('example'); + } + } + + $this->load->view('example/forgot_password'); + } + + /** + * Example::change_password() + * + */ + public function change_password($code = '') + { + if( ! $user = $this->bitauth->get_user_by_forgot_code($code)) + { + redirect('example'); + } + + if($this->input->post()) + { + $this->form_validation->set_rules('password', 'Password', 'required|bitauth_valid_password'); + $this->form_validation->set_rules('password_conf', 'Password Confirmation', 'required|matches[password]'); + + if($this->form_validation->run() == TRUE) + { + $this->bitauth->save_new_password($this->form_validation->set_value('password'), $code); + redirect('example'); + } + } + + $this->load->view('example/change_password', array('forgot_code' => $code)); } /** diff --git a/application/language/english/bitauth_lang.php b/application/language/english/bitauth_lang.php index a0c9dba..faaf15b 100644 --- a/application/language/english/bitauth_lang.php +++ b/application/language/english/bitauth_lang.php @@ -62,3 +62,23 @@ $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!)'; + +/** + * Email Activation Messages + */ +$lang['bitauth_activation_email_subject'] = 'Activation email'; +$lang['bitauth_activation_email_message'] = 'Dear User,

Please click on the following link to activate your account!

Your account details:
Username: %s
Password: %s

%s

Thank you'; +$lang['bitauth_activation_email_send_error'] = 'Could not send activation email, please contact the webmaster'; + +/** + * Forgot Password Messages + */ +$lang['bitauth_forgotpassword_email_subject'] = 'Forgotten password reset email'; +$lang['bitauth_forgotpassword_email_message'] = 'Dear User,

This is a password reset confirmation email.

%s'; +$lang['bitauth_forgotpassword_email_send_error'] = 'Could not send password reset email, please contact the webmaster'; + +/** + * Recatpcha Messages + */ +$lang['bitauth_recaptcha_error'] = 'Incorrect captcha code entered. Try again!'; +$lang['bitauth_recaptcha_regenerate'] = 'I can\'t read, please generate new captcha!'; \ No newline at end of file diff --git a/application/libraries/Bitauth.php b/application/libraries/Bitauth.php index e130b9f..bf42d85 100644 --- a/application/libraries/Bitauth.php +++ b/application/libraries/Bitauth.php @@ -55,6 +55,11 @@ public function __construct() $this->_remember_token_expires = $this->config->item('remember_token_expires', 'bitauth'); $this->_remember_token_updates = $this->config->item('remember_token_updates', 'bitauth'); $this->_require_user_activation = $this->config->item('require_user_activation', 'bitauth'); + $this->_email_activation = $this->config->item('email_activation', 'bitauth'); + $this->_activation_email_address = $this->config->item('activation_email_address', 'bitauth'); + $this->_mailtype = $this->config->item('mailtype', 'bitauth'); + $this->_locked_out_alert_message = $this->config->item('locked_out_alert_message', 'bitauth'); + $this->_locked_out_notify_address = $this->config->item('locked_out_notify_address', 'bitauth'); $this->_pwd_max_age = $this->config->item('pwd_max_age', 'bitauth'); $this->_pwd_age_notification = $this->config->item('pwd_age_notification', 'bitauth'); $this->_pwd_min_length = $this->config->item('pwd_min_length', 'bitauth'); @@ -306,6 +311,23 @@ public function locked_out() if($this->timestamp(strtotime($last->time), 'U') - $this->timestamp(strtotime($first->time), 'U') <= ($this->_mins_login_attempts * 60) && $this->timestamp(strtotime($last->time), 'U') >= $this->timestamp(strtotime($this->_mins_login_attempts.' minutes ago'), 'U')) { + if($this->_locked_out_alert_message) + { + $config['useragent'] = 'bitauth'; + $config['mailtype'] = $this->_mailtype; + $this->email_lib->initialize($config); + $this->email_lib->clear(); + $this->email_lib->from($this->_locked_out_notify_address); + $this->email_lib->to($this->_locked_out_notify_address); + $this->email_lib->subject('Invalid login attempt on '.base_url()); + $this->email_lib->message('User: '.$username.' IP address: '.$_SERVER['REMOTE_ADDR'].' Time: '.mdate("%Y-%m-%d %H:%i:%s", time())); + + if( ! $this->email_lib->send()) + { + log_message('error', 'Invalid login attempt email send failed.'.$this->email_lib->print_debugger()); + } + } + return TRUE; } } @@ -498,6 +520,11 @@ public function add_user($data, $require_activation = NULL) if($require_activation) { $data['activation_code'] = $this->generate_code(); + + if($this->_email_activation) + { + $this->_send_email_activation($data['email'], $data['activation_code'], $data['username'], $data['password']); + } } // Just in case @@ -840,6 +867,52 @@ public function forgot_password($user_id) return FALSE; } + /** + * Bitauth::generate_forgot_code() + * + * Sends a generated forgot code to the give email address + */ + public function generate_forgot_code($email) + { + if( ! $user = $this->get_user_by_email($email)) + { + return FALSE; + } + $forgot_code = $this->forgot_password($user->user_id); + + $config['useragent'] = 'bitauth'; + $config['mailtype'] = $this->_mailtype; + $this->email_lib->initialize($config); + $this->email_lib->clear(); + $this->email_lib->from($this->_activation_email_address); + $this->email_lib->to($email); + $this->email_lib->subject($this->lang->line('bitauth_forgotpassword_email_subject')); + $this->email_lib->message(sprintf($this->lang->line('bitauth_forgotpassword_email_message'), + 'Click here to reset your password')); + + if( ! $this->email_lib->send()) + { + log_message('error', $this->email_lib->print_debugger()); + show_error($this->lang->line('bitauth_forgotpassword_email_send_error')); + exit; + } + } + + /** + * Bitauth::save_new_password() + * + * Saves a newly entered password, and delete forgot_code + */ + public function save_new_password($password, $code) + { + if( ! $user = $this->get_user_by_forgot_code($code)) + { + return FALSE; + } + $this->set_password($user->user_id, $password); + $this->update_user($user->user_id, array('forgot_code' => '')); + } + /** * Bitauth::set_password() * @@ -1572,6 +1645,9 @@ public function _assign_libraries() $CI->load->library('encrypt'); $this->encrypt = $CI->encrypt; + $CI->load->library('email'); + $this->email_lib = $CI->email; + $this->load->database(); $this->db = $CI->db; @@ -1593,4 +1669,31 @@ public function _assign_libraries() } + /** + * Bitauth::_send_email_activation() + * + * Send activation email to activate user account + */ + public function _send_email_activation($user_email, $activation_code, $username, $password) + { + $config['useragent'] = 'bitauth'; + $config['mailtype'] = $this->_mailtype; + $this->email_lib->initialize($config); + $this->email_lib->clear(); + $this->email_lib->from($this->_activation_email_address); + $this->email_lib->to($user_email); + $this->email_lib->subject($this->lang->line('bitauth_activation_email_subject')); + $this->email_lib->message(sprintf($this->lang->line('bitauth_activation_email_message'), + $username, + $password, + 'Click here to activate')); + + if( ! $this->email_lib->send()) + { + log_message('error', $this->email_lib->print_debugger()); + show_error($this->lang->line('bitauth_activation_email_send_error')); + exit; + } + } + } \ No newline at end of file diff --git a/application/libraries/Recaptcha.php b/application/libraries/Recaptcha.php new file mode 100644 index 0000000..b8f9d39 --- /dev/null +++ b/application/libraries/Recaptcha.php @@ -0,0 +1,292 @@ + $value ) + $req .= $key . '=' . urlencode( stripslashes($value) ) . '&'; + + // Cut the last '&' + $req=substr($req,0,strlen($req)-1); + return $req; + } + + + + /** + * Submits an HTTP POST to a reCAPTCHA server + * @param string $host + * @param string $path + * @param array $data + * @param int port + * @return array response + */ + private function _recaptcha_http_post($host, $path, $data, $port = 80) { + + $req = $this->_recaptcha_qsencode ($data); + + $http_request = "POST $path HTTP/1.0\r\n"; + $http_request .= "Host: $host\r\n"; + $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n"; + $http_request .= "Content-Length: " . strlen($req) . "\r\n"; + $http_request .= "User-Agent: reCAPTCHA/PHP\r\n"; + $http_request .= "\r\n"; + $http_request .= $req; + + $response = ''; + if( false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) ) ) { + die ('Could not open socket'); + } + + fwrite($fs, $http_request); + + while ( !feof($fs) ) + $response .= fgets($fs, 1160); // One TCP-IP packet + fclose($fs); + $response = explode("\r\n\r\n", $response, 2); + + return $response; + } + + + + /** + * Gets the challenge HTML (javascript and non-javascript version). + * This is called from the browser, and the resulting reCAPTCHA HTML widget + * is embedded within the HTML form it was called from. + * @param string $pubkey A public key for reCAPTCHA + * @param string $error The error given by reCAPTCHA (optional, default is null) + * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false) + + * @return string - The HTML to be embedded in the user's form. + */ + public function recaptcha_get_html ($pubkey, $error = null, $use_ssl = false) + { + if ($pubkey == null || $pubkey == '') { + die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); + } + + if ($use_ssl) { + $server = RECAPTCHA_API_SECURE_SERVER; + } else { + $server = RECAPTCHA_API_SERVER; + } + + $errorpart = ""; + if ($error) { + $errorpart = "&error=" . $error; + } + return ' + + '; + } + + + /** + * Calls an HTTP POST function to verify if the user's guess was correct + * @param string $privkey + * @param string $remoteip + * @param string $challenge + * @param string $response + * @param array $extra_params an array of extra variables to post to the server + * @return ReCaptchaResponse + */ + public function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $extra_params = array()) + { + if ($privkey == null || $privkey == '') { + die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); + } + + if ($remoteip == null || $remoteip == '') { + die ("For security reasons, you must pass the remote ip to reCAPTCHA"); + } + + + + //discard spam submissions + if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) { + $recaptcha_response = new ReCaptchaResponse(); + $recaptcha_response->is_valid = false; + $recaptcha_response->error = 'incorrect-captcha-sol'; + return $recaptcha_response; + } + + $response = $this->_recaptcha_http_post (RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify", + array ( + 'privatekey' => $privkey, + 'remoteip' => $remoteip, + 'challenge' => $challenge, + 'response' => $response + ) + $extra_params + ); + + $answers = explode ("\n", $response [1]); + $recaptcha_response = new ReCaptchaResponse(); + + if (trim ($answers [0]) == 'true') { + $recaptcha_response->is_valid = true; + } + else { + $recaptcha_response->is_valid = false; + $recaptcha_response->error = $answers [1]; + } + return $recaptcha_response; + + } + + /** + * gets a URL where the user can sign up for reCAPTCHA. If your application + * has a configuration page where you enter a key, you should provide a link + * using this function. + * @param string $domain The domain where the page is hosted + * @param string $appname The name of your application + */ + public function recaptcha_get_signup_url ($domain = null, $appname = null) { + return "https://www.google.com/recaptcha/admin/create?" . $this->_recaptcha_qsencode (array ('domains' => $domain, 'app' => $appname)); + } + + private function _recaptcha_aes_pad($val) { + $block_size = 16; + $numpad = $block_size - (strlen ($val) % $block_size); + return str_pad($val, strlen ($val) + $numpad, chr($numpad)); + } + + /* Mailhide related code */ + + private function _recaptcha_aes_encrypt($val,$ky) { + if (! function_exists ("mcrypt_encrypt")) { + die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed."); + } + $mode=MCRYPT_MODE_CBC; + $enc=MCRYPT_RIJNDAEL_128; + $val=$this->_recaptcha_aes_pad($val); + return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); + } + + + private function _recaptcha_mailhide_urlbase64 ($x) { + return strtr(base64_encode ($x), '+/', '-_'); + } + + /* gets the reCAPTCHA Mailhide url for a given email, public key and private key */ + public function recaptcha_mailhide_url($pubkey, $privkey, $email) { + if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) { + die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " . + "you can do so at http://www.google.com/recaptcha/mailhide/apikey"); + } + + + $ky = pack('H*', $privkey); + $cryptmail = $this->_recaptcha_aes_encrypt ($email, $ky); + + return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . $this->_recaptcha_mailhide_urlbase64 ($cryptmail); + } + + /** + * gets the parts of the email to expose to the user. + * eg, given johndoe@example,com return ["john", "example.com"]. + * the email is then displayed as john...@example.com + */ + private function _recaptcha_mailhide_email_parts ($email) { + $arr = preg_split("/@/", $email ); + + if (strlen ($arr[0]) <= 4) { + $arr[0] = substr ($arr[0], 0, 1); + } else if (strlen ($arr[0]) <= 6) { + $arr[0] = substr ($arr[0], 0, 3); + } else { + $arr[0] = substr ($arr[0], 0, 4); + } + return $arr; + } + + /** + * Gets html to display an email address given a public an private key. + * to get a key, go to: + * + * http://www.google.com/recaptcha/mailhide/apikey + */ + public function recaptcha_mailhide_html($pubkey, $privkey, $email) { + $emailparts = $this->_recaptcha_mailhide_email_parts ($email); + $url = recaptcha_mailhide_url ($pubkey, $privkey, $email); + + return htmlentities($emailparts[0]) . "...@" . htmlentities ($emailparts [1]); + + } + + +} + +/** + * A ReCaptchaResponse is returned from recaptcha_check_answer() + */ +class ReCaptchaResponse { + var $is_valid; + var $error; +} \ No newline at end of file diff --git a/application/views/example/add_user.php b/application/views/example/add_user.php index 41ab339..ce78bcd 100644 --- a/application/views/example/add_user.php +++ b/application/views/example/add_user.php @@ -15,6 +15,7 @@ BitAuth: <?php echo $title; ?> + + '; + echo 'BitAuth Example: '.$title.''; + echo 'Username'.form_input('username', set_value('username')).''; + echo 'Full Name'.form_input('fullname', set_value('fullname')).''; + echo 'Email'.form_input('email', set_value('email')).''; + echo 'Password'.form_password('password').''; + echo 'Confirm Password'.form_password('password_conf').''; + echo 'Captcha code

'.$this->lang->line('bitauth_recaptcha_regenerate').''.form_input(array('name' => 'recaptcha_response_field', 'id' => 'recaptcha_response_field', 'value' => set_value('recaptcha_response_field'))).''; + + if(validation_errors()) + { + echo ''.validation_errors().''; + } + + echo ''.form_submit('submit',$title).''; + echo ''; + echo form_close(); + + echo $recaptcha; + + echo '
'; + if(isset($bitauth) && $bitauth->logged_in()) + { + echo anchor('example/logout', 'Logout', 'style="float: right;"'); + } + else + { + echo anchor('example/login', 'Login', 'style="float: right;"'); + } + echo '
'; + + ?> + +