1. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,959
    Rep Power
    1014
    What's your code?

    The principle is rather simple: You store the timestamp of when the token has been created. To check it, you fetch the timestamp, parse it (preferably with the DateTime class, on old PHP setups with strptime()) and then compare it to the current time. If the token is older than, say, 30 minutes, it's no longer valid:

    PHP Code:
    <?php

    // from database
    $test_time '2013-04-27 11:11:59';

    $created DateTime::createFromFormat('Y-m-d G:i:s'$test_time);

    if ( 
    $created >= new DateTime('30 minutes ago') )
        echo 
    'valid';
    else
        echo 
    'expired';;
    Note: Just like with the login form, you must not tell the visitor whether the email address even exists. If it doesn't exist or the token doesn't exist or it has expired, you display a generic error message saying something like "The reset token is invalid or has expired. Click here to request a new token".

    The reason I'm saying this is because I've seen a lot of websites (even "professional" ones) that get this wrong. So while the login form protects the email addresses, the reset form doesn't -- which of course makes the whole thing useless.
    Last edited by Jacques1; April 27th, 2013 at 05:23 AM.
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  2. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    451
    Rep Power
    8
    ok, here are the scripts. the first one is where you request the recovery, and the second is the response (where you change your password).

    But there still is the problem with the time, it says the token is expired (when debugging)... Do I have to 'require()' the DateTime?

    Here are the scripts:

    forgot_password.php:
    PHP Code:
    <?php 

       
        
    require("common.php"); 
        require(
    "lib/rnum.php");
        require(
    "lib/mail.php");
        require(
    "lib/password.php"); 

       
        
    $submitted_username ''
        if(!empty(
    $_POST)) 
        { 
            
            
    $query 
                SELECT 
                    username, 
                    id,
                    email 
                FROM users 
                WHERE 
                    username = :username 
            "

             
           
            
    $query_params = array( 
                
    ':username' => $_POST['username'
            ); 
             
           
               
                
    $stmt $db->prepare($query); 
                
    $result $stmt->execute($query_params); 
           
           
                
    $row $stmt->fetch(); 
                if(
    $row
                { 
                    if (
    $_POST['email'] == $row['email'])
                    {
                        
    //A mail has been sent
                        
    $email $row['email'];
                        
    $password_token rnum();
     
                        
    $query 
                            DELETE FROM responses
                            WHERE user = :user
                        "

             
           
                        
    $query_params = array( 
                        
    ':user' => $row['id'
                        ); 
             
           
               
                        
    $stmt $db->prepare($query); 
                        
    $result $stmt->execute($query_params); 

                          
    $query 
                        INSERT INTO responses ( 
                             reset_key,
                            user,
                            secret,
                            request_timestamp,
                            request_ip
                        ) VALUES ( 
                            :reset_key,
                            :user,
                            :secret,
                            :request_timestamp,
                            :request_ip
                        )"
    ;
     
                        
    $secret password_hash($password_tokenPASSWORD_BCRYPT, array("cost" => 10));  
                        
    $reset_key rnum();
                        
    $user $row['id'];
                        
    $request_timestamp DateTime("Y-m-d G:i:s");
                        
    $request_ip getenv('REMOTE_ADDR');

                          
    $query_params = array( 
                            
    ':reset_key' => $reset_key,
                            
    ':user' => $user,
                            
    ':secret' => $secret,
                            
    ':request_timestamp' => $request_timestamp,
                            
    ':request_ip' => $request_ip
                        
    ); 
             
           
                        
    $stmt $db->prepare($query); 
                        
    $result $stmt->execute($query_params); 
            

                        
    $url 'http://www.domain.com/response_forgot_password.php?reset_key=';
                        
    $url .= $reset_key;
                        
    $url .= '&user=';
                        
    $url .= $user;
                        
    $url .= '&password_token=';
                        
    $url .= $password_token;

                        
    $mail_to      $email;
                        
    $mail_subject 'Forgot password';
                        
    $mail_body 'Click this link to reset your password: ';
                        
    $mail_body .= $url;

                        
    mail_f ($mail_to$mail_subject$mail_body);
                    }
            } 
           else {
                    echo 
    "Please enter your email and username."
           }
        } 
         
    ?> <html>
    <body>
    <form action="forgot_password.php" method="post"> 
        Username: 
        <input type="text" name="username" value="<?php echo $submitted_username?>" /> 
        <br /><br /> 
        Email:
        <input type="text" name="email" value="" /> 
        <br /><br /> 
        <input type="submit" value="Login" /> 
    </form> 
    </body>
    </html>
    response_forgot_password.php:

    PHP Code:
    <?php 

        
        
    require("common.php"); 
        require(
    "lib/password.php");


        
    $reset_key $_GET["reset_key"];
        
    $user $_GET["user"];
        
    $password_token $_GET["password_token"];
        

        if(!empty(
    $_POST)) 
        { 
            
    $reset_key $_POST['reset_key'];
            
    $user $_POST['user'];
            
    $password_token $_POST['password_token'];

            
    $query 
            SELECT 
                user,
                secret,
                request_timestamp,
                used,
                active
            FROM responses
            WHERE 
                reset_key = :reset_key
            "

             
            
    $query_params = array( 
                
    ':reset_key' => $reset_key
            
    ); 
             
           
            
    $stmt $db->prepare($query); 
            
    $result $stmt->execute($query_params); 
            
            
    $row $stmt->fetch(); 

            if(
    $row['active'] == 1)
            {
                if(
    $row['used'] == 0)
                {
                    if(
    $row['user'] == $user)
                    {
                        
    $created DateTime::createFromFormat('Y-m-d G:i:s'$row['timestamp']); 
                        if ( 
    $created >= new DateTime('30 minutes ago') ) 
                        {
                            if ( 
    password_verify($password_token$row['secret']) )
                            {
                                   
    $query 
                                UPDATE users 
                                SET 
                                    password = :password
                                WHERE
                                id = :id 
                                "

                
                                  
    $hash password_hash($_POST['password'], PASSWORD_BCRYPT, array("cost" => 10));         
             
                                
    $query_params = array( 
                                    
    ':password' => $hash
                                    
    ':id' => $row['user']
                                ); 
             
             
                                    
    $stmt $db->prepare($query); 
                                
    $result $stmt->execute($query_params); 
                            } 
                        } 
                         else
                        {
                            echo 
    "This token has already been used, expired or inactive, please request a new one";
                        }
                    }
                }
                else
                {
                    echo 
    "This token has already been used, expired or inactive, please request a new one";
                }
            }
            else
            {
                echo 
    "This token has already been used, expired or inactive, please request a new one";
            }
        }
    ?> <html>
    <body>
                                           
    <form action="response_forgot_password.php" method="post">  
     New password:
        <input type="text" name="password" value="" /> 
        <br /><br /> 
    <input type="hidden" name="reset_key" value="<?php echo $reset_key?>" />
    <input type="hidden" name="user" value="<?php echo $user?>" />
    <input type="hidden" name="password_token" value="<?php echo $password_token?>" />
        <input type="submit" value="Login" /> 
    </form> 

    </body>
    </html>
  3. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,959
    Rep Power
    1014
    The code shouldn't even run, because you're using the DateTime class wrong. This is a class, not a function. I wouldn't even use PHP to get the current timestamp. Just call NOW() in MySQL.

    There are some other issues:


    "forgot password":

    • The form lets anybody check the existence of user accounts. This is not as bad as exposing email addresses, but it's not a good idea either.
    • The error message for wrong usernames is pretty misleading (apart from the fact that it shouldn't even exist).
    • If the email address is wrong, the form doesn't do anything. Which means the user will wait for the email forever.
    • Why even require the user name? Just ask for the email address. If it exists, you send the reset token. If it doesn't exist, you send an email explaining that the address does not exist on your system. This way you don't expose any information and still maintain high usability.


    "response":

    The "if-else" logic is unnecessarily bloated. You seem to have forgotten about the logical operators && (and) and || (or) for combining multiple conditions. Instead of making three(!) nested "if" parts and repeating the same "else" action three times, why not just check all conditions at once?

    PHP Code:
    if ($cond_1 && $cond_2 && $cond_3)
    {
        
    // do something
    }
    else
    {
        
    // do something else

    instead of

    PHP Code:
    if ($cond_1)
    {
        if (
    $cond_2)
        {
            if (
    $cond_3)
            {
                
    // do something
            
    }
            else
            {
                
    // do something else
            
    }
        }
        else
        {
            
    // do something else
        
    }
    }
    else
    {
        
    // do something else

    Actually, all those conditions may very well be in the WHERE clause of the query:

    sql Code:
    SELECT  
    	USER, 
    	, secret
    	, request_timestamp
    FROM
    	responses 
    WHERE  
    	reset_key = :reset_key
    	AND USER = :USER
    	AND NOT used
    	AND active
    ;

    You're not supposed to delete the old tokens when a new one is created. That breaks the whole idea of logging them. You're supposed to set "active" to 0.
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  4. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    451
    Rep Power
    8
    Originally Posted by Jacques1
    Why even require the user name? Just ask for the email address. If it exists, you send the reset token. If it doesn't exist, you send an email explaining that the address does not exist on your system. This way you don't expose any information and still maintain high usability.
    So I have to send an email to the submitted address in any case, but then it could be used as a kind of spam generator I would think.

    But why shouldn't we check first the email if it belongs to a user but make no notification of it. And how would you try to solve the problem of the misspelled email?

    Could you also post the DateTime class?
  5. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,959
    Rep Power
    1014
    Password resets are inherently problematic, and there's not much you can do about that.

    Yes, you can choose to do nothing in case of a wrong email address. But this means there's no error feedback. Imagine a user with multiple email addresses (not all that rare nowadays). He/she is sure to have registered with address_1, when it fact it's address_2. In your case, he/she would wait forever for the email. This can be very frustrating, especially if you want quick access to your account again.

    Actually, you couldn't give any feedback at all, because a message like "email has been sent" would either be a lie, or it would expose the existence of the address.

    Not exactly a good approach.

    If you're worried about your form being abused, I think the best workaround would be to limit the number of emails that are sent to a "nonexistent" mail to 1 per day (or something like that). Of course this means you have to log the failed requests as well.



    Originally Posted by derplumo
    Could you also post the DateTime class?
    It's a built-in PHP class:
    http://www.php.net/manual/en/datetime.construct.php
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  6. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    451
    Rep Power
    8
    ok so what should I adjust to the password recovery to make it work like it should? But how do I use DateTime exactly then?

    Hope we can make the password recovery work within a couple of days...
  7. No Profile Picture
    Registered User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Apr 2013
    Posts
    2
    Rep Power
    0
    Originally Posted by E-Oreo
    You should not store the hashed password in a cookie ever. There is a thread floating around here somewhere that discusses the concepts behind a "remember me" feature in more detail, but from a high level design there are two safe approaches you can take:

    (Note: a "token" in this case just means a long unique random string of characters)

    (1) Using randomly generated tokens

    First you would create a new database table that stores (at least) three pieces of data in each row: a remember me token, a user id and an expiration date. The remember me token column should be unique, and so can serve as the primary key for the table.

    When a user logs in with a "remember me" option enabled, you would generate a new random token (the process of generating a random token is identical to the process used to generate a random salt in the register.php file), insert the token into that new database table along with an expiration date and user ID, then set a cookie on the user's computer containing that token.

    When an unauthenticated user visits the site, you would look for the cookie, and if it contains a valid and unexpired remember me token, you would re-authenticate them automatically by setting the appropriate session variables (as is done in login.php).

    .
    I am a beginner. I have the following questions:
    - why should the token be unique?
    - should we also hash the token when storing it in the database? Cause I've read that it is like the password..
    - do we also have to take care of the risk that user's cookie could be stolen? Do we need to record any other information (like IP address) to make it more secure?
    - should one user only have one token or should we allow one for multiple tokens (to allow "remember me" in multiple computers). What's the best practice?

    Thank you very much!
  8. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,959
    Rep Power
    1014
    I know people love this "remember me" stuff and think they need it (because the other websites have it as well). But when you think about it, it's a really bad idea. Not only is it unsecure, it actually teaches your members unsecure behaviour. Many users just intuitively click on this checkbox without realizing this will generate an infinite session and basically bypass the whole authentication mechanism.

    So instead of writing yet another implementation and jumping through hoops to get it halfway secure, why not promote a secure alternative from the beginning? There are several excellent browser plugins to automatically fill in the user credentials. For example, Chrome has ChromeIPass.

    If your users are too lazy for this and you really, really need a "remember me" checkbox despite the massive security issues, I'd offer something like a "long session" as a compromise: The session would last for several hours and get extended after every action. So when you're using the site regularly, you'll always be logged in. But the session won't last forever, it does expire after a long period of inactivity.
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  9. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    451
    Rep Power
    8
    Jacques, what should I adjust to the password recovery to make it work like it should? And how do I use DateTime exactly?
  10. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    451
    Rep Power
    8
    And what are the disadvantages of using the IP adresses for a remember me feature? just curious
  11. No Profile Picture
    Lost in code
    Devshed Supreme Being (6500+ posts)

    Join Date
    Dec 2004
    Posts
    8,316
    Rep Power
    7171
    Responding to posts in reverse order:

    And what are the disadvantages of using the IP adresses for a remember me feature? just curious
    It depends on exactly how you're using them, however in general the primary disadvantages of relying on IP addresses for anything are:
    1) They change frequently
    2) They are easily spoofed

    ----

    And how do I use DateTime exactly?
    PHP Code:
    $dbdate "2007-01-01 21:00:23"// Retrieve this value from your database; I'm assuming this is the time the token was created at.
    $dbdate_object = new DateTime($dbdate); // This converts the string from the database into a date object which PHP can use for comparisons (you can't compare a string)
    $compare_object = new DateTime('-30 minutes'); // This creates a second date object which represents the time from 30 minutes ago

    // This checks to see whether the date from the database is older than the time that it was 30 minutes ago.
    if($dbdate_object $compare_object) {
      
    // token is expired

    ----

    I am a beginner. I have the following questions:
    - why should the token be unique?
    - should we also hash the token when storing it in the database? Cause I've read that it is like the password..
    - do we also have to take care of the risk that user's cookie could be stolen? Do we need to record any other information (like IP address) to make it more secure?
    - should one user only have one token or should we allow one for multiple tokens (to allow "remember me" in multiple computers). What's the best practice?
    1) Because if two different users have the same token you wouldn't know which one to log in
    2) Yes hashing it is a good idea. It is like the password in that having the original value will give you access to the site as a particular user; it is unlike the password in that having the value does not compromise the user's account on any other sites, and if a breach does occur you can reset it without majorly inconveniencing your users.
    3) The user's cookie being stolen is the major risk of any remember me system. Relying on the IP address is not very accurate, but you could include the user's browser user agent as part of the hash. Obviously if you actually store the user agent in plaintext, which most systems do, then it's not very difficult for the attacker to obtain that information anyway.
    4) Normally you would allow multiple tokens, because users frequently have more than one computer or device and would want the feature to work on all of them at the same time.

    ----

    So I have to send an email to the submitted address in any case
    No, there is no reason at all to ever send an email to an address that isn't registered. As Jacques1 stated, you have two options:
    1) Lie and say the email was sent anyway; this is more secure, but less helpful to a legitimate user.
    2) Tell the user if the address isn't registered; this is less secure, but more helpful to a legitimate user. Most sites accept this compromise and go with this option.
    PHP FAQ

    Originally Posted by Spad
    Ah USB, the only rectangular connector where you have to make 3 attempts before you get it the right way around
  12. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,959
    Rep Power
    1014
    Originally Posted by derplumo
    Jacques, what should I adjust to the password recovery to make it work like it should?
    Well, just like I explained above. The "request" part would look something like this:

    PHP Code:
    if(!empty($_POST['email']))  
    {  
        
    $user_stmt $db->prepare('
            SELECT
                id
            FROM
                users
            WHERE
                email = :email
        '
    );
        
    $user_stmt->execute(array(
            
    ':email' => $_POST['email']
        ));
        
    $user_id $user_stmt->fetchColumn();
        if (
    $user_id)
        {
            
    // user exists: deactivate all current requests and insert a new one
            
    $deactivation_stmt $db->prepare('
                UPDATE
                    responses
                SET
                    active = 0
                WHERE
                    user = :user
            '
    );
            
    $deactivation_stmt->execute(array(
                
    ':user' => $user_id
            
    ));
            
    // insert new request with NOW() as the creation time
            // send mail with reset key and secret
        
    }
        else
        {
            
    // user does not exist: send a mail explaining that this address isn't registered    
        
    }
        echo 
    'An email with further instruction has been sent to ' htmlentities($_POST['email'], ENT_QUOTESENT_HTML401'UTF-8');

    And for the response part, you can use the query from #198. The rest you can take from your current code (the DateTime check and the password check).



    Originally Posted by derplumo
    And how do I use DateTime exactly?
    You used it correctly in the reset part. You just need to change the request part so that the new row will have NOW() as the creation time.



    Originally Posted by E-Oreo
    No, there is no reason at all to ever send an email to an address that isn't registered.
    Yes, there is. See below.



    Originally Posted by E-Oreo
    2) Tell the user if the address isn't registered; this is less secure, but more helpful to a legitimate user. Most sites accept this compromise and go with this option.
    In my opinion, this is not an acceptable solution. Making the email address "public" is a violation of your users' privacy. Imagine some delicate website about politics, health or whatever. I do not want my boss or anybody else to check whether I'm registered on that site.

    Actually, many of the websites that use the "compromise" you're talking about are not really thought through. Most developers understand that the login form must not differentiate between a nonexistant account and a wrong password, so they only display a generic message like "wrong username and/or password". But when the reset form does let me check the username "through the backdoor", the whole privacy protection of the login form is just useless.

    The only way to both protect privacy and maintain usability is to send an email in any case. If you're worried about abuse, you can limit the number of emails going to a "foreign" account to one per day (or even less).

    You may also put an "unsubscribe" link into those emails. Just track the emails and see if they're misused. If they are, you can rethink your strategy and add captchas, security questions or whatever.
    Last edited by Jacques1; April 29th, 2013 at 08:12 PM.
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  13. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    451
    Rep Power
    8
    Ok thank you very much, but you use the query's in a different way than I'm using now, which is better?
    Last edited by derplumo; April 30th, 2013 at 03:01 PM.
  14. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,959
    Rep Power
    1014
    Different in what way?
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  15. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    451
    Rep Power
    8
    yours:

    PHP Code:
    [U]$user_stmt $db->prepare(' [/U]
            SELECT 
                id 
            FROM 
                users 
            WHERE 
                email = :email 
        '
    ); 
        [
    U]$user_stmt->execute(array( [/U]
            
    ':email' => $_POST['email'
        )); 
        [
    U]$user_id $user_stmt->fetchColumn();[/U
    and the way i'm using it now:
    PHP Code:
    [U]$query =[/U
                SELECT 
                    username, 
                    id,
                    email 
                FROM users 
                WHERE 
                    username = :username 
            "

             
           
            [
    U]$query_params[/U] = array( 
                
    ':username' => $_POST['username'
            ); 
             
           
               
            [
    U]$stmt $db->prepare($query); 
            
    $result $stmt->execute($query_params); 
           
            
    $row $stmt->fetch(); [/U
    I guess the second way is just longer...

IMN logo majestic logo threadwatch logo seochat tools logo