Question

I'm curious to know: once a pw_reset token is sent via email, is it possible to get the email address of the referrer's account for validating, or would it be best to validate against an IP? I've set my token expire for only 1 hour, and even dynamic IPs don't change at that rate, right?

Some code I'm playing around with (very much WIP), open to improvements/suggestions/critique.

Thanks in advance.

 /**
 * Reset Password Form
 * 
 * $route['auth/reset-password'] = 'reset_password_form';
 * 
 */
public function reset_password_form()
{
    $this->load->view('templates/public', array(
        'content'   =>  'auth/reset_password_form',
        'title' =>  'Password reset',
        'description'   =>  '',
        'canonical' =>  'auth'
    ));
}
/**
 * Reset Password Authentication
 * 
 * $route['auth/reset_password/validate'] = 'auth/reset_password';
 * 
 */
public function reset_password()
{
    //setup validation rules
    $config = array(
        array('field'=>'email_address', 'label' =>'Email address', 'rules'=>'trim|required|max_lenght[128]|valid_email|valid_user_email')
    );
    $this->form_validation->CI =& $this; 
    $this->form_validation->set_rules($config); 

    //run validator and confirm OR
    if($this->form_validation->run($this))
    {

        //create the token data
        $tokenhash = Auth::create_token();
        $password_token = json_encode(array(
            'token'      => $tokenhash,
            'expires'    => date('h:i:s A', strtotime($this->config->item('pw_token_expires'))), // default 1 hours
            'ip_address' =>  $this->input->ip_address()
        )); // output {"token":"3513f5ee34ED3","expires":"01:14:06 AM","ip_address":"127.0.0.1"}


        //grab a userid to use via php-activerecord custom USER model
        $user = User::get_by_email_address($this->input->post('email_address'));

        //update the user pw_token field
        try{
            if($user->update_attribute('pw_token', $password_token))
            {
                throw new \ActiveRecord\ActiveRecordException($user->errors);
            }
        }catch(\ActiveRecord\ActiveRecordException $e){
            log_message('error', $e->getMessage());
        }

        //setup email data
        //TODO : move this to USER activeRecord\Model (pre_)
        $email_data = array(
            'email_to'  =>  $user->email_address,
            'token'     =>  anchor(site_url('auth/reset_password/confirm/'.$tokenhash.'/'.$user->id.''), 'click here to reset your password'),
            'site_link' =>  anchor(site_url(), site_url())
        );

        try{
            if(Mail::send($email_data, 'reset_password.tpl.html'))
            {
                throw new Exception(Mail::errors());
            }
        }catch(Exception $e){
            log_message('error', $e->getMessage());
        }

    }
    else
    {
        $this->reset_password_form();
    }
}
/**
 * Reset Password Confirmation
 * 
 * $route['auth/reset_password/confirm/(:any)/(:num)'] = 'auth/confirm_password_reset';
 */
public function confirm_password_reset($token='', $userid='')
{
    //check for null values
    if(!$token || !$id)
    {
        redirect('/');
    }

    //ugly
    $attempts = $this->session->set_userdata('reset_pw_attempt', (int)0);
    $this->session->set_userdata('reset_pw_attempts', $attempts++);



    if(!$this->user->id != $userid && $this->session->userdata('logged_in') === (int)0)
    {
        //not great but cant validate the email, so we check to see if they have a logged in session.
        //this is not a "forgot_password request" so we should be good as long as they are logged in
        show_404();
    }
    else
    {

        //looking good so far, now lets see do they have the correct permissions
        if(in_array(PERM_UPDATE, Auth::permissions($this->user->permissions)))
        {
            if($attempts == (int)3)
            {
                $this->session->set_flashdata('', $this->lang->line('pw_reset_attempt_over'));
                redirect('/');
            }
            else
            {
                $tokn[] = json_decode($this->user->pw_token);

                if(date('h:i:s A', time()) > $tokn['expires'] && $token===$tokn['token'])
                {
                    $this->load->view('do-pw_reset_form');
                }
            }
        }
        else
        {
            $this->session->set_flashdata('info', $this->lang->line('update_permission_denied'));
            redirect('/');
        }
    }
}
Was it helpful?

Solution

Checking the email address via a querystring parameter or utilizing IP restriction are both completely superfluous. If the password token is random and long enough it will be impossible to brute-force it, especially when combined with a token expiration AND rate-limiting of token guesses.

The IP address restriction can be a usability issue as well. For instance a user might request a password reset just before leaving work then finish the reset process from home or during the commute home - that is, the user's IP address changes between the reset request and the reset confirmation.

OTHER TIPS

Why would you want to check the referrer? Using a closed loop verification is a standard practice for confirming user email addresses. You don't need to check IP or referrer, all you need to do is create a custom hash and track the email address, then send the email with this information to the user.

When the user clicks the embedded link you then confirm the hash you created, if the hash line up, then the user has confirmed their email account with your system.

//Example Link
https://mydomain.com/verify?hash=<some_hash>

For added security you can also track the time your system sent the email, and invalidate the hash after 24 hours. So if the user clicks after 25 hours, you inform them the hash is invalid and ask if they want another email sent, if so, repeat the above process.

There is no reason to check for the referrer, if the client's computer is compromised there is nothing you can do to avoid a Hijack. What you can do is watch out for CSRF vulnerabilities.

Also, after reading it it looks like it does check for the 'ip_address' => $this->input->ip_address().

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top