Jump to content

activation email and password resetting


ajoo

Recommended Posts

Hi all !

 

It looks to me that sending an activation email and resetting a password are more or less similar operations as both require a token to be returned to the website for verification and thereafter changing the password.

 

The two cannot be confused since password resetting must obviously always, if at all, occur after account activation. The record in the database would in any case be deleted after any of these operations have been executed successfully. So I was wondering if it is alright to use the same table in the database for both these operations. Or do we need to retain some information in the database after these operations are completed. Information that can be handy later for some operations I cannot think of right now.

 

I have noticed that google asks for any old password that a user can recall. What do they do with that?  How can they identify a user with that I wonder ? They wouldn't be storing plain passwords would they? 

 

Thanks all !

 

 

 

 

 

 

Link to comment
Share on other sites

Not sure what record you would be deleting in the database. A 'user' record, once established, should remain so that it doesn't have to be re-created with the user's help. Doing an account activation and a reset are similar as you suspect and no, the passwords would never be in clear text, if done correctly. Why do you think google is doing something like that? One doesn't submit a hashed password. The script receiving it does the hash before querying the db.

Link to comment
Share on other sites

Hi  ginerjm, 

 

Thanks for the revert.  Since the token, once verified & used is no longer required, so I thought if I would create a new table (say temp ) to keep that data ( token with a time stamp) and I would update the activate field in the users table ( the main table with user details. ) Once it's been activated ( for account activation ), the values in the temp table are no longer needed and so that can be deleted. In fact I delete the record as soon as the activation has been achieved. 

 

 

 

Why do you think google is doing something like that?

 

I have no idea so I asked. If I forgot my plain text password and google has no way of knowing it since they don't store plain passwords, how would an old password help? Probably they have the hashes of old passwords stored. 

 

So once again, would this be a good or a bad idea to delete the records of the temporary tokens and timestamps for the same.

 

Thanks loads !

Link to comment
Share on other sites

The answer is "it depends". It depends on what you want to do. In Googles case they are apparently maintaining a history of the passwords used. I'm a little surprised by that, but it is not an uncommon process for enterprise level software that prevents users from reusing the last n number of passwords. If you do not plan on utilizing any feature where old passwords are needed then you don't need to maintain that data.

 

The same logic applies to your question regarding the token - with one caveat. DELETE operations are expensive in terms of performance. If you don't need to delete a piece of data, then don't bother. Since the registration and reset processes will both use a token, that means that all users will need a token at some point. Plus, I would assume that no user would ever need two tokens concurrently. To keep things simple, you could simply have a token field in the "user" table. After someone has used a token, set the token field to an empty string or a NULL value (or setting the extirpation date to null or a prior date would work too).

 

I would also expect that you also have some data in the table to know if a user has been registered or not: password confirmed field, registration Boolean, etc. So, when the token is used you could use that data to determine whether to run the registration logic or the password reset logic.

  • Like 1
Link to comment
Share on other sites

I agree with Psycho's response and the idea of saving on the delete. Just add the token field to your one table, along with a time field for it to limit the amount of time the user has to respond to his/her activation/reset email.

Link to comment
Share on other sites

First off: You only store the token hashes, right? Tokens are equivalent to a password (at least the reset tokens), so they must not be stored as plaintext.

 

Storing the token hashes in a separate table is in fact potentially dangerous, because it can lead to a situation where an attacker creates multiple reset requests for a victim account and then tries to guess the token. This is hopeless if you use a proper random number generator, but it can happen if there are weaknesses in the procedure. To be sure, a new token hash should always overwrite the previous hash. The easiest way to implement that is to store everying in the user table.

 

Mixing the activation token with the reset token is also a bad idea. Sure, the verification process is somewhat similar, but the importance of the tokens is vastly different. While the activaction token isn't critical for security, knowing the reset token gives you full access to the account. So the tokens should be strictly separated. Do not reuse any fields in the table.

  • Like 1
Link to comment
Share on other sites

Thank you Guru Jacques for clarifying the subtle but huge differences between the two procedures. 

 

 

because it can lead to a situation where an attacker creates multiple reset requests for a victim account and then tries to guess the token.

 

Are you suggesting that checking and changing (resetting to NULL once used) the token be an atomic operation? I have also tried to use a bit of logic which prevents multiple tokens from being generated till a certain time period has lapsed. 

 

I was thinking of keeping the tokens and its time stamp in separate table and updating the account activation field in the users table once the token had been authenticated and also deleting that row with the token and time stamp from the table (with just token & ts ) all at one go. Reason being that

 

1. the tokens once used are not needed and are in fact a security hazard if kept. 

2. only relevant data is stored in the users table preventing clutter. 

 

After psycho advised against using the delete operations I am thinking of forgetting about the deletions and just setting the tokens to NULL.

 

Please advise. 

 

Thank you. 

Edited by ajoo
Link to comment
Share on other sites

I personally wouldn't store the tokens - any any form - in the DB.  For password reset I would have them generated on the fly based on a hash of the account ID, email and date requested.  This makes the link valid until midnight server time. For account activation I would do the same without the date part and using a slightly different hashing algorithm.

 

That's just me though, I mainly write internal apps, not open ones. This means I have a more controlled user set so can make decisions based on a narrower group of variables.

 

If you do go down the route of storing the tokens in the DB then you shouldn't really be deleting them, they should be stored in hashed form and flagged with a status.  Let the table grow, it can help establish useful metrics on account usage and can be used to flag up suspicious behaviour.  You can archive it off on a monthly, quarterly or yearly basis, but I wouldn't throw away data on user activity - not in this day and age.

  • Like 1
Link to comment
Share on other sites

Thanks Guru Jacques & Funster for the inputs.

 

Funster if you don't store the tokens, then what does the link compare to once it's been clicked by the user? I believe it's not possible to retrieve the data once it has been hashed.

 

Any particular reason for for being so averse to storing tokens.

 

Guru Jacques, I'll read up on the link and revert. 

 

Thank you both.

Link to comment
Share on other sites

A hash of the user ID and timestamp is a very bad “secret”. All of this data is public or easy to find out, which makes it trivial to brute-force the token. In fact, if the attacker himself requested the password reset, he knows the exact timestamp as well as the user ID. If that's your approach, you might as well allow anybody to take over any account. It's effectively the same.

 

The token must be secret and impossible to guess. As I already said, it's equivalent to a password. And you wouldn't generate your password from a timestamp.

 

 

 

If you do go down the route of storing the tokens in the DB then you shouldn't really be deleting them, they should be stored in hashed form and flagged with a status.

 

This simply doesn't work in a web application where many concurrent processes access the same table at (almost) the same time. While you're still busy invalidating the current tokens and inserting a new one, there may be hundreds of new requests coming in. The result will be complete nonsense – or worse, a security vulnerability.

 

Maybe this approach “works” with a handful of well-intentioned internal users, but it definitey doesn't work on the Internet.

  • Like 1
Link to comment
Share on other sites

It goes to a script that replicates the token creation using the email address.

 

e.g for password reset:

 

Hash a token based on UUID, and date now (eg 01/01/1970)

Send token to email address.

Have link pass email address and token to script

Have script create a hash based on the UUID attached to the email address and the current date

Compare tokens.

If tokens = match present reset form.

 

That's pretty much how I do it.

 

@ Jacques1 : that's not strictly true.  The token doesn't contain any human friendly content, there is no way to discern from looking at it what has been used to generate it.  It is also sent to the email address of the user, so unless the attacker already has access to the users emails it's redundant. User ID's are not generally available and I also use custom hashing for each process. The target script also looks for a match against a hash generated within it.  So to brute force it someone would need to know the UID they want to exploit, the exact hashing algorithm I used and the internal salt string (plus any other data that I may or may not decide to use to build the initial hash depending on my mood at time of coding).  Then they would need to recreate the output and push that exactly to the target script.  OK, for a web facing product that may not be secure enough - I'm no expert on that, but it more than covers the skill set of the users I cater for.

 

Again, I don't prescribe this approach for a public system, and have little experience of coding for such an environment, I only offer this by way of information, not suggestion.

 

However, I don't see how, if properly designed, there would be nonsense in the table because of transaction volume.  It's only a password reset table, I can't envisage that many people needing to reset their password simultaneously as to break a table.  Surely a table structure akin to would be a functional transaction table?

 

||RID||UID||DATE||TOKEN||STATUS||TIMESTAMP_CREATED||TIMESTAMP_UPDATED||UPDATE_COUNT||

 

From that you can get, regardless of how many people reset their password in a given millisecond, a gauge of how many users reset a password in a given month, how often the 10 most reset passwords were reset over a given time period, if any accounts have requested multiple resets within a time period, how many reset requests on average and/or by max and minimum an account places before converting to a successful password reset.  etc. etc.

Link to comment
Share on other sites

Your approach boils down to “security through obscurity”: Instead of using a proven solution, you try to irritate attackers with all kinds of “custom algorithms” and weird procedures, assuming that those are hard to find out.

 

It has been demonstrated many times that this doesn't work. One of the PHP core developers actually put up a challenge where people had to guess custom hash algorithms from the hashes alone. From 15 algorithms, 14 were guessed correctly – in a short amount of time by people who did it just for fun. So I definitely would not try this challenge in a real application.

 

Obscurity is actually dangerous, because “secret algorithms” are by definition neither properly reviewed nor tested. I've seen quite a lot of custom algorithms of all kinds, and almost all of them had fatal design flaws as well as implementation defects. Critical software must be peer-reviewed, ideally by thousands of experts all around the world. This has happened with the random number generator of your operating system. It has not happened and will never happen with your own algorithm.

 

Good code doesn't have to be hidden. In fact, the best implementations I know are all publicy available on GitHub.

Link to comment
Share on other sites

I agree, what I do isn't the best way of doing things.  That's the primary reason I try to explain my methods as being just that: my methods.  I'm not looking to convert others to working the way I do by any means, but information is information - they even teach psychology in this country by spending the first year giving you the wrong information, then spend the next three telling you that it was wrong and why.  My putting up information on my methods allows those better than me at this - case in point being you yourself - the opportunity to open up a dialogue that others can see and be part of on why these things are not a good idea, thus providing help and insight.

 

It's just nice when it's done in a positive and constructive way :)

 

I do what I do because it's the quickest and easiest way to get the results I require within the constraints my workplace puts on me.  I'm not a dedicated coder, I don't make that as an excuse - as much as it sounds like it at this point - but I can only do what I can do and no more.  If I have to have something live within a time frame then I will assess security in relation to the environment and code to the standard I asses to be necessary.  This approach is obviously null and void when applying it to web facing projects.

Link to comment
Share on other sites

Hi Guru Jacques and Funster, 

 

Thanks for the replies and the discussion.

 

Your discussion on the various techniques will go a long way to clear the doubts of many New comers like me, especially for issues that are related to security so closely. 

 

Thanks very much. I think I will stick with the advise offered by Guru Jacques since that relates more to the security of applications on the internet. I think I have made up my mind, with the input from all who replied to this, to keep it simple, all in one table.

 

This is how I think I will do it:

 

Have just one field for the hashed token and time stamp which I guess can be used both for account activation and password reset.

There can be no confusion in the tokens since the password reset can only take place after the account activation.

Have 2 boolean fields: one for account activation and one for password resetting.

The password reset field can be toggled for each valid password reset request. And later reset back once the password has successfully changed.

Once the account is activated i'll change the related token field to null.

Same for password reset.

 

So that would be a total of 4 additional fields in the table. 

 

If there is still a flaw here in the logic, kindly alert me.

 

Thanks a lot every one.  

Link to comment
Share on other sites

 

 

 I think I will stick with the advise offered by Guru Jacques

 

I would hope so too ;) , you should almost always deffer to those with guru status here - it's not something that is given lightly on these boards and I would say that @Jacques1 has clearly displayed he is well versed in this area.

 

All the best with your implementation.

Link to comment
Share on other sites

Hi Psycho, 

 

Thanks for the advice. I think you are referring to a second channel of authentication like maybe via an sms.

 

As of now, I would be more than pleased if I can just get this logic to be implemented correctly. Certainly I would like to implement that at a later stage maybe. I have that in mind already.

 

Thanks again !

Link to comment
Share on other sites

Since @muddy_funster brought up a specific case that I was going to post about, I will post here. Especially interested in @Jaques1 feedback.

 

In the case where you want to track the reset data you obviously would need a separate password reset table. My question is, what would be the best/most secure way to handle it.

 

Currently my forgot password reset code will add a row with the proper data needed for a reset along with an expiration datetime. On successful reset only the hash is deleted leaving only tracking data. Any hashes not reset are not usable after the timeout.

 

The following code was originally written several years ago with only the hashing and password generation parts updated per the recommended code from @Jaques1.

 

Anyone see any problems or recommend any changes?

 

forgot.php

<?php
/*
 * Last Modified <!--%TimeStamp%-->6/23/2016 10:39 AM<!---->
 */

require('./config.php');

$show_error = false;
if (!empty($_POST))
    {
    //------------------------------------------------------------------------
    // Trim $_POST Array
    //------------------------------------------------------------------------

    $_POST = trim_array($_POST);

    //------------------------------------------------------------------------
    // Validate Form Input
    //------------------------------------------------------------------------

    $error = array();

    if (empty($_POST['forgot']))
        {
        $error['forgot'] = 'Email Required.';
        }

    //------------------------------------------------------------------------
    // Check for errors
    //------------------------------------------------------------------------

    if (count($error))
        {
        $show_error = true;
        }
    else
        {
        // Check DB for matching username and password.
        $sql = "SELECT user_id, email FROM users WHERE email = ?";

        $stmt = $pdo->prepare($sql);
        $stmt->execute(array(
            $_POST['forgot']
        ));
        $row = $stmt->fetch();

        //---------------------------------------------------------------------------------------------
        // No Results - Redirect
        //---------------------------------------------------------------------------------------------

        if (!$stmt->rowCount())
            {
            die(header("Location: {$_SERVER['SCRIPT_NAME']}?fail"));
            }

        //---------------------------------------------------------------------------------------------
        // Check Reset table to see if there are multiple incomplete reset attempts
        // Block access if too many.
        //---------------------------------------------------------------------------------------------

        $user_id = $row['user_id'];
        /*
        $sql = "SELECT COUNT(*) from password_reset WHERE user_id = ? AND password_reset_key  <>'' " ;
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array(
        $user_id
        ));

        $count = $stmt->fetchColumn();
        if ($count==3){
        echo '<div class="error_custom">Too Many Incomplete Password Resets. Contact Site Admin</div>';
        //echo $count;
        exit;
        }
        */
        //---------------------------------------------------------------------------------------------
        // Log Password Reset Data
        //---------------------------------------------------------------------------------------------

        // From http://forums.phpfreaks.com/topic/298729-forgotten-password/?hl=%2Bmcrypt_create_iv#entry1524084
        // generate 16 random bytes
        $raw_token = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);

        // encode the random bytes and send the result to the user
        $encoded_token = bin2hex($raw_token);

        // hash the random bytes and store this hash in the database
        $token_hash = hash('sha256', $raw_token);

        /**
         * Interval specification.
         *
         * The format starts with the letter P, for "period." Each duration period is
         * represented by an integer value followed by a period designator. If the
         * duration contains time elements, that portion of the specification is
         * preceded by the letter T.
         *
         * @link http://www.php.net/manual/en/dateinterval.construct.php
         *
         * String to time option
         * $password_reset_expiration_datetime = date('Y-m-d H:i:s', strtotime("+5 min"));
         *
         */
        $period_designator = 'PT';
        $timespan          = 'M'; //Minutes
        //$period_designator = 'P'; $timespan ='D'; //Days
        $timespan_add      = 1; // Amount of days or minutes

        $time = new DateTime(date('Y-m-d H:i:s'));
        $time->add(new DateInterval($period_designator . $timespan_add . $timespan));

        $password_reset_expiration_datetime = $time->format('Y-m-d H:i:s');

        $sql  = "INSERT INTO password_reset (user_id, password_reset_username_email, password_reset_requesters_ip, password_reset_key,password_reset_expiration_datetime) values(?, ?, ?, ?, ?)";
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array(
            $user_id,
            $_POST['forgot'],
            $_SERVER["REMOTE_ADDR"],
            $token_hash ,
            $password_reset_expiration_datetime
        ));

        //---------------------------------------------------------------------------------------------
        // Email Reset Data
        //---------------------------------------------------------------------------------------------

        $mail_to        = $row['email'];
        $mail_from_name = "Meet Market";
        $mail_subject   = "Lost Password";
        $mail_message   = "You have requested a forgotten password reset." . PHP_EOL . PHP_EOL;

        $mail_message .= "Click the link below or enter the following code on the Password Reset page. Reset Code: $encoded_token\n$url_website/reset.php?k=$encoded_token";

        // Send mail
        mail($mail_to, $mail_subject, $mail_message, "From: $mail_from_name <$email_from>\r\n");

        die(header("Location: reset.php?sent"));

        if (DEBUG == 1)
            {
            debug_show_sql();
            }

        } // End if
    } // End (!empty($_POST))



include('./includes/header.php');
?>
<div class="container">
<?php
if (isset($_GET['fail']))
    {
    echo '<div class="error_custom">Invalid Username or Email</div>';
    }

//--------------------------------------------------------------------
// Display Logo
//--------------------------------------------------------------------

logo();

//---------------------------------------------------------------------------------------------
// Forgot Password Form
//---------------------------------------------------------------------------------------------

if ($show_error)
    {
    show_form_errors($error);
    }
?>
<form class="form-horizontal" action="<?= $_SERVER['SCRIPT_NAME'] ?>" method="post">
   <div class="form-group <?= !empty($error['forgot']) ? 'has-error' : '' ?>">
      <label class="col-md-4 control-label" for="forgot">Enter Email</label>
      <div class="col-md-4">
         <input id="forgot" name="forgot" type="text" placeholder="Email Address" class="form-control input-md">
         <span class="help-block">A password reset email will be sent to you.</span>
      </div>
   </div>
   <div class="form-group">
      <div class="col-md-offset-4  col-sm-10">
         <button id="submit" type="submit" name="submit" class="btn btn-primary">Reset Password</button>  <a href="./login.php">Login</a>
      </div>
   </div>
</form>

</div><!--/ container -->

<?php
include('./includes/footer.php');
?>

reset.php

<?php
/*
 * Last Modified <!--%TimeStamp%-->3/11/2016 9:20 PM<!---->
 */

session_start();
require('./config.php');

if ($_POST)
    {
    //------------------------------------------------------------------------
    // Trim $_POST Array
    //------------------------------------------------------------------------

    $_POST = trim_array($_POST);

    //------------------------------------------------------------------------
    // Validate Form Input
    //------------------------------------------------------------------------

    if (empty($_POST['reset_code']))
        {
        $error['reset_code'] = 'Reset Code required.';
        }

    if (empty($_POST['new_password']))
        {
        $error['new_password'] = 'New Password is required.';
        }

    if (empty($_POST['new_password_confirm']))
        {
        $error['new_password_confirm'] = ' Confirm New Password is required.';
        }
    elseif ($_POST['new_password'] != $_POST['new_password_confirm'])
        {
        $error['new_password_confirm'] = 'Passwords do not match.';
        }

    //---------------------------------------------------------------------------------------------
    // Check for errors
    //---------------------------------------------------------------------------------------------

    if (count($error))
        {
        $show_error = true;
        }
    else
        {
        // From http://forums.phpfreaks.com/topic/298729-forgotten-password/?hl=%2Bmcrypt_create_iv#entry1524084
        $encoded_token = $_POST['reset_code'];

        // decode the token and hash it
        $raw_token  = hex2bin($encoded_token);
        $token_hash = hash('sha256', $raw_token);

        // Check DB for matching reset key.
        $sql  = "SELECT user_id, password_reset_username_email, password_reset_key FROM password_reset WHERE password_reset_key=?";
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array(
            $token_hash
        ));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        //---------------------------------------------------------------------------------------------
        // No Results - Redirect
        //---------------------------------------------------------------------------------------------

        if (!$stmt->rowCount())
            {
            die(header("Location: {$_SERVER['SCRIPT_NAME']}?fail"));
            }

        //---------------------------------------------------------------------------------------------
        // Update Password
        //---------------------------------------------------------------------------------------------

        $hashed_new_password = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
        $sql                 = "UPDATE users SET password= ? WHERE user_id = ?";
        $stmt                = $pdo->prepare($sql);
        $stmt->execute(array(
            $hashed_new_password,
            $row['user_id']
        ));

        /**
         * Delete used reset key. We are leaving unused keys to be able to see how
         * many times a user requests a reset before they complete a password reset.
         * Since reset keys are only valid for limited time there is no risk leaving
         * the unused keys.
         *
         * TODO: DEV: Might be a good idea to block password resets after X times of not
         * completing a password reset. i.e. User has submitted 3 password reset requests
         * and hasnt entered reset key and new password. They are now blocked. Alert admin.
         *
         * $sql = "SELECT COUNT(*) from password_reset WHERE user_id = ? AND password_reset_key  <>'' " ;
         *
         **/

        $sql  = "UPDATE password_reset SET password_reset_key = ? WHERE user_id = ? AND password_reset_key = ?";
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array(
            NULL,
            $row['user_id'],
            $row['password_reset_key']
        ));

        if (DEBUG == 1)
            {
            debug_show_sql();
            }

        //---------------------------------------------------------------------------------------------
        // Send Reset Email
        //---------------------------------------------------------------------------------------------

        $mail_to        = $row['password_reset_username_email'];
        $mail_from_name = "Some Site";
        $mail_subject   = "Password has been reset";
        $mail_message   = "Password has been reset";

        // Send mail
        mail($mail_to, $mail_subject, $mail_message, "From: $mail_from_name <$email_to>\r\n");

        die(header("Location: login.php?reset"));

        } // End if ($validated)
    } // End (!empty($_POST))

//---------------------------------------------------------------------------------------------
// Reset Code Form
//---------------------------------------------------------------------------------------------

include('./includes/header.php');

if (isset($_GET['fail']))
    {
    echo '<div class="error_custom">Invalid code or time expired</div>';
    }

logo(); // Display Logo

if (isset($_GET['sent']))
    {
    echo '<div class="success">A reset code has been been emailed to you. Enter code below to reset your password or click link in email.</div>';
    }

isset($show_error) ? show_form_errors($error) : ''; // Display Form errors if any

if (isset($_GET['k']))
    {
    $reset_code = $_GET['k'];
    }
if (isset($_POST['reset_code']))
    {
    $reset_code = $_POST['reset_code'];
    }
?>
<form class="form-horizontal" action="<?= $_SERVER['SCRIPT_NAME'] ?>" method="post">

   <div class="form-group <?= !empty($error['reset_code']) ? 'has-error' : '' ?>">
      <label class="col-md-4 control-label" for="reset_code">Reset Code</label>
      <div class="col-md-5">
         <input id="reset_code" name="reset_code" type="text" placeholder="Reset Code" class="form-control input-md" value="<?= !empty($reset_code) ? htmlspecialchars($reset_code) : '' ?>">
      </div>
   </div>

   <div class="form-group <?= !empty($error['new_password']) ? 'has-error' : '' ?>">
      <label class="col-md-4 control-label" for="new_password">Enter New Password</label>
      <div class="col-md-5">
         <input id="new_password" name="new_password" type="text" placeholder="Enter New Password" class="form-control input-md" value="<?= !empty($_POST['new_password']) ? htmlspecialchars($_POST['new_password']) : '' ?>">
      </div>
   </div>

   <div class="form-group <?= !empty($error['new_password_confirm']) ? 'has-error' : '' ?>">
      <label class="col-md-4 control-label" for="new_password_confirm">Confirm New Password</label>
      <div class="col-md-5">
         <input id="new_password_confirm" name="new_password_confirm" type="text" placeholder="Confirm New Password" class="form-control input-md" value="<?= !empty($_POST['new_password_confirm']) ? htmlspecialchars($_POST['new_password_confirm']) : '' ?>">
      </div>
   </div>

   <div class="form-group">
      <div class="col-md-offset-4 col-sm-10">
         <button id="submit" type="submit" name="submit" class="btn btn-primary">Reset Password</button>  <a href="./login.php">Login</a>
      </div>
   </div>

</form>
<?php
include('./includes/footer.php');
?>
Edited by benanamen
Link to comment
Share on other sites

In the case where you want to track the reset data you obviously would need a separate password reset table. My question is, what would be the best/most secure way to handle it.

 

Currently my forgot password reset code will add a row with the proper data needed for a reset along with an expiration datetime. On successful reset only the hash is deleted leaving only tracking data. Any hashes not reset are not usable after the timeout.

 

As discussed previously, there should never be more than one 'active' hash for a user at a time, so the 'easiest' thing to do is to store it in the user table. You can still track the activity of requesting a password reset, resetting the password, etc. in a separate table. There's no reason the hash has to be in that table. In fact, there are likely activities you may want to track that have no hash as part of that activity. E.g. unsuccessful logins. So, I would keep the hash in the User table and just log the events in a separate table. That table might look something like this

 

id | event_type_ID | userID | IP_address | timestamp

 

The event_type_ID would be a foreign key back to a table with a defined list of events:

Login Successful

Login Failed (No matching User ID)

Login Failed (Wrong Password)

Password Reset Request

Account Locked

Login w/ Temp Password

Password Changed

 

etc. Those are only examples and would depend on the business requirements of your application. You could use the data to proactively prevent unauthorized access.

 

FYI: The two "Login Failed" messages above would only be for internal use. You would never provide the user whether the User ID or password were incorrect.

Link to comment
Share on other sites

 

 

 In fact, there are likely activities you may want to track that have no hash as part of that activity. E.g. unsuccessful logins.

Funny you should say that. I already do that. It is part if the login script that I have not posted. I log all login attempt statuses, good or bad, what the bad user and pass was and IP and datetime.

 

As I think about it, it wouldnt take much modification to track just the reset data. Table is already there, just need to remove the hash and expiration columns and put them in the users table. 

Link to comment
Share on other sites

You actually can keep the tokens in a separate table as long as you have a one-to-one or one-to-many relationship between the tokens and the users (i. e. you store the current token ID in the user table rather than the other way around).

 

Trying to hide the existence of usernames is futile. For example, bcrypt will cause a huge delay during the password check, so it's immediately obvious if the name or the password is wrong. You could try to prevent this by hashing a dummy password if the name is already wrong, but this doesn't really fix the problem. There may still be suble timing differences or other factors which reveal the control flow (like errors which only happen in a particular branch).

 

I think a much better approach is to not use secret names in the first place. Instead of, for example, (mis)using the e-mail address, simply make the user choose a public name.

Link to comment
Share on other sites

For one system where I needed tokens for various reasons, I stored the tokens into a separate table along with a small bit of data. I had a separate tokens table with columns: TokenId, TokenHash, CreatedOn, ExpiresOn, RedirectTo, ContextData, InvalidatedOn

 

The basic premise of how it all worked was that wherever a token was needed, I'd call a class which would generate the token and store it, giving the calling code back the ID and plain text value.

 

All the token validation was handled by one controller with a URL such as http://example.com/validate-token/$token. That controller would verify the token exists and hasn't expired / been invalidated and if so add the token to a list of validated tokens in the current session. If the token validates successfully it then redirected to the controller associated with the token.

 

The final controller then looks up the token from the session and can use the associated context data to perform whatever it needs to do.

 

The code ended up looking something like (symfony based):

Reset request controller:

$tokenManager = $this->getTokenManager();
$context = new ForgotPassword($user);
$tokenOptions = new TokenOptions(new \DateInterval('PT3H'), $context);
$token = $tokenManager->createToken('forgot_password_reset', $tokenOptions);

$code = $token->getTokenPlaintext();
$verifyUrl = $tokenManager->generateUsageUrl($token);

//Email user
Validate token controller:

$tokenManager = $this->getTokenManager();
$token = $tokenManager->validateToken($tokenPlainText);
if (!$token){
    return $this->invalidResponse();
}

return $this->redirectToRoute($token->getRedirectTo());
Reset controller:

$token = $this->locateToken(); //Searches the validated tokens list for one with a ForgotPassword context
/** @var ForgotPassword $context */
$context = $token->getContextData();
$user = $em->find('AppBundle:User', $context->userId);
//handle reset for user.
//once reset is complete:
$this->getTokenManager()->invalidateToken($token);
I've found it to work fairly well so far. The same system handles forgotten password requests, email validations, invitations, temporary access to documents, etc.
Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.