Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.4k views
in Technique[技术] by (71.8m points)

authentication - PHP login system: Remember Me (persistent cookie)


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Update (2017-08-13): To understand why we're separating selector and token, instead of just using a token, please read this article about splitting tokens to prevent timing attacks on SELECT queries.

I'm going to extract the strategy outlined in this blog post about secure long-term authentication since that covers a lot of ground and we're only interested in the "remember me" part.

Preamble - Database Structure

We want a separate table from our users' table that looks like this (MySQL):

CREATE TABLE `auth_tokens` (
    `id` integer(11) not null UNSIGNED AUTO_INCREMENT,
    `selector` char(12),
    `token` char(64),
    `userid` integer(11) not null UNSIGNED,
    `expires` datetime,
    PRIMARY KEY (`id`)
);

The important things here are that selector and token are separate fields.

After Logging In

If you don't have random_bytes(), just grab a copy of random_compat.

if ($login->success && $login->rememberMe) { // However you implement it
    $selector = base64_encode(random_bytes(9));
    $authenticator = random_bytes(33);

    setcookie(
        'remember',
         $selector.':'.base64_encode($authenticator),
         time() + 864000,
         '/',
         'yourdomain.com',
         true, // TLS-only
         true  // http-only
    );

    $database->exec(
        "INSERT INTO auth_tokens (selector, token, userid, expires) VALUES (?, ?, ?, ?)", 
        [
            $selector,
            hash('sha256', $authenticator),
            $login->userId,
            date('Y-m-dTH:i:s', time() + 864000)
        ]
    );
}

Re-Authenticating On Page Load

if (empty($_SESSION['userid']) && !empty($_COOKIE['remember'])) {
    list($selector, $authenticator) = explode(':', $_COOKIE['remember']);

    $row = $database->selectRow(
        "SELECT * FROM auth_tokens WHERE selector = ?",
        [
            $selector
        ]
    );

    if (hash_equals($row['token'], hash('sha256', base64_decode($authenticator)))) {
        $_SESSION['userid'] = $row['userid'];
        // Then regenerate login token as above
    }
}

Details

We use 9 bytes of random data (base64 encoded to 12 characters) for our selector. This provides 72 bits of keyspace and therefore 236 bits of collision resistance (birthday attacks), which is larger than our storage capacity (integer(11) UNSIGNED) by a factor of 16.

We use 33 bytes (264 bits) of randomness for our actual authenticator. This should be unpredictable in all practical scenarios.

We store an SHA256 hash of the authenticator in the database. This mitigates the risk of user impersonation following information leaks.

We re-calculate the SHA256 hash of the authenticator value stored in the user's cookie then compare it with the stored SHA256 hash using hash_equals() to prevent timing attacks.

We separated the selector from the authenticator because DB lookups are not constant-time. This eliminates the potential impact of timing leaks on searches without causing a drastic performance hit.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
...