Question

I'm writing a simple password-recovery function for the website I'm developing and I was wondering about the expire time.

Getting to the point, I want to add an expire time of around 48h for the reset password link I'm gonna send. Do I have to create a new column to store the current time and check it out some time later to see if its still valid, or is there a simpler way?

That's my code so far:

public function forgotPass($email) {
    $bd = new Bd();
    $conn = $bd->connect();
    $stt = $conn->prepare("SELECT * FROM Users where email=?");
    $stt-> bind_param("s",$email);
    $stt-> execute();
    $result = $stt->get_result();
    if (mysqli_num_rows($result) == 1) {
        $stt = $conn->prepare("INSERT INTO Users(recovery) VALUES(?)");
        $recovery = $this->randHash(8);

        if (!$recovery)
            return false;

        $stt-> bind_param("s",$recovery);
        $stt-> execute();
    }
}

and here's my randHash code:

private static function randHash($lenght) {
    if (!filter_var($lenght, FILTER_VALIDATE_INT)) {
        return false;
    }   
    $allowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $max = strlen($allowed) - 1;
    for ($i=1;$i<=$lenght;$i++) {
        $hash .= $allowed[mt_rand(0, $max)];
    }
    return $hash;
}
Was it helpful?

Solution

Just save the expiration time with the reset token in the database, and when the time has expired just don't accept the reset token anymore. This is by far the easiest and safest method.

Another way would be creating a reset hash, appending the time, and encrypting that with a secret key. Decrypt and check the timestamp when you check the hash. If the key leaked, however, this method becomes as weak as just putting it in plain text in the URL.

OTHER TIPS

Storing the current date in database is one way to go. Then you can easily check if its less then 48 hours off. Another way to go is to include the time in the email.

public function forgotPass($email) {
    $bd = new Bd();
    $conn = $bd->connect();
    $stt = $conn->prepare("SELECT * FROM Users where email=?");
    $stt-> bind_param("s",$email);
    $stt-> execute();
    $result = $stt->get_result();
    if (mysqli_num_rows($result) == 1) {
        $hash1 = md5(microtime().$email.'xx'); //create a unique number for email
        $ctime = time();
        $hash2 = md5($hash1.$ctime); //create a unique hash based on hash1 and time

        $stt = $conn->prepare("INSERT INTO Users(recovery) VALUES(?)");
        $recovery = $hash2;

        if (!$recovery)
            return false;

        $stt-> bind_param("s",$recovery);
        $stt-> execute();

        //send email with link
        // http://www.example.com/resetpass.php?hash=$hash1&time=$ctime
    }
}

//and then in resetpass.php
//NEEDS CHECKS FOR VALID QUERYVALUES

if (time()-$_GET['time'] <= 48 * 60 * 60)  {
  $checkhash = md5($_GET['hash'].$_GET['time']);
  //check database for hash
}

EDIT: I received an email this morning regarding this answer and asking whether or not the hashing-part of my answer is just for making a really unique ID. The following is my response:

I went through and re-read the question on Stack Overflow. The code is not just for making a really unique ID (although it is important that there be no collisions). It is also to make it very hard for someone else to write their own password recovery link to gain access to a user account.

By creating a hash with a username and email (and without hashing the entire code), we would be able to include an additional validation on the user's identity. That is, merely having the link wouldn't be enough; a valid username and email address combination would also be needed to reset a password.

Strings are prepended to username, current time and email in a basic attempt to defeat rainbow tables. In retrospect, it would be better to replace the " " salts with base64_encode($row['username']) and base64_encode($email) respectively.


I would suggest creating two columns: recovery and recovery_expiry. Recovery_expiry holds when the link expires, and recovery holds the hash that must be compared. It consists of the username, a salt appended by the current time, and the current email address of the user.

function forgotPass($email)
{
    $currTime = time();
    $expiryTime = 60 * 60 * 24 * 2; // Two days

    $bd = new Bd();
    $conn = $bd->connect();
    $stt = $conn->prepare("SELECT * FROM Users where email=?");
    $stt->bind_param("s", $email);
    $stt->execute();
    $result = $stt->get_result();
    if (mysqli_num_rows($result) == 1)
    {
        $row = mysqli_fetch_array();
        $stt = $conn->prepare("INSERT INTO Users(recovery, recovery_expiry)"
               . " VALUES(?,?)");
        $hash = hash("sha256", " " . $row['username'])
                . hash("sha256", "vivid" . $currTime)
                . hash("sha256", " " . $email);

        $stt->bind_param("s", $hash);
        $stt->bind_param("i", $currTime + $expiryTime);
        $stt->execute();
    }
    else
    {
        // Return that the given email address did not match any records
    }
    // Here would be the logic to send the forgotten password link to the user
}

function checkHash($hash)
{
    $row = null;
    $currTime = time();
    $bd = new Bd();
    $conn = $bd->connect();
    $stt = $conn->prepare("SELECT * FROM Users WHERE recovery=? AND recovery_expiry < $currTime");
    $stt->bind_param("s", $hash);
    $stt->execute();
    $result = $stt->get_result();
    if (mysqli_num_rows($result) == 1)
    {
        $row = mysqli_fetch_array();
    }
    return $row;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top