delight-im / PHP-Auth

Authentication for PHP. Simple, lightweight and secure.
MIT License
1.08k stars 234 forks source link

Random User Logout and Cookie Anomalies #289

Open zerofruchtshake opened 1 year ago

zerofruchtshake commented 1 year ago

Hello,

I'm currently experiencing a random logout issue while using the PHP-Auth library in my application, predominantly on Windows machines. macOS users do not seem to experience this problem. The symptoms also include anomalies with the user session cookies:

Anomalies: Users are randomly logged out, without any discernible pattern or trigger. A specific session cookie seems to behave oddly: Name: remember_6TpGxxxxxxx Value: %7E (Notably shorter than typical values I observe when logged in.) Expires: 2024-07-16T20:43:45.520Z (This is definitely incorrect as the expiry date gets updated to a correct value when the user logs in again.) These issues occur when running a Tampermonkey script with Chrome on an external domain (example1.com) to interact with the login-protected domain (example2.com).

Despite preliminary debugging efforts using browser developer tools and server-side logging, the root cause of this issue remains elusive...

If anyone could provide some guidance or hints on where to start further debugging, I would greatly appreciate it. Thank you in advance for your assistance.

ocram commented 1 year ago

Thank you for bringing this up and for sharing all the details!

  1. The value ~ (%7E) in a remember_* cookie is definitely not an expected value, as that cookie is supposed to contain a long random string.
  2. Why do you think 2024-07-16 is not a valid expiry date, and what kind of date do you usually see there after logging in?
  3. Does all the weird behavior just occur when running the Tampermonkey script with Chrome, and you don’t notice it otherwise?
zerofruchtshake commented 1 year ago

2) because remember-me duration should be either 12 or 48h if ($_POST['remember'] == 1) { $rememberDuration = (int) (172800); } else { $rememberDuration = (int) (43200); } when logging in, it is set correctly: 2023-07-24T10:07:46.565Z

3) i'll test some more. it definitely does not happen on MacOS (including using the same Tampermonkey script in Chrome)

zerofruchtshake commented 1 year ago

I tried adding some log statements to find the cause:

Auth.php

private function setRememberCookie($selector, $token, $expires) {
        writeToLog("setRememberCookie() called. Selector: " . $selector . ", Token: " . $token . ", Expires: " . $expires);
        $e = new \Exception();
        writeToLog("Stack trace: " . $e->getTraceAsString());
        $params = \session_get_cookie_params();
        if (isset($selector) && isset($token)) {
            $content = $selector . self::COOKIE_CONTENT_SEPARATOR . $token;
        } else {
            $content ='';
        }

        // save the cookie with the selector and token (requests a cookie to be written on the client)
        writeToLog("Creating new Cookie object with name: " . $this->rememberCookieName);
        $cookie = new Cookie($this->rememberCookieName);
        $cookie->setValue($content);
        $cookie->setExpiryTime($expires);
        $cookie->setPath($params['path']);
        $cookie->setDomain($params['domain']);
        $cookie->setHttpOnly($params['httponly']);
        $cookie->setSecureOnly($params['secure']);
        $result = $cookie->save();
        writeToLog("Cookie saved. Value: " . $cookie->getValue() . ", Expiry time: " . $cookie->getExpiryTime() . ", Path: " . $cookie->getPath() . ", Domain: " . $cookie->getDomain());

        if ($result === false) {
            throw new HeadersAlreadySentError();
        }

        // if we've been deleting the cookie above
        if (!isset($selector) || !isset($token)) {
            // attempt to delete a potential old cookie from versions v1.x.x to v6.x.x as well (requests a cookie to be written on the client)
            writeToLog("Deleting old cookie with name: 'auth_remember'");
            $cookie = new Cookie('auth_remember');
            $cookie->setPath((!empty($params['path'])) ? $params['path'] : '/');
            $cookie->setDomain($params['domain']);
            $cookie->setHttpOnly($params['httponly']);
            $cookie->setSecureOnly($params['secure']);
            $cookie->delete();
            writeToLog("Old cookie 'auth_remember' deleted");
        }
    }

LOGS:

2023-07-22 13:14:43 setRememberCookie() called. Selector: , Token: , Expires: 1721582083
2023-07-22 13:14:43 Stack trace: #0 /var/www/html/vendor/delight-im/auth/src/Auth.php(157): Delight\Auth\Auth->setRememberCookie()
#1 /var/www/html/vendor/delight-im/auth/src/Auth.php(69): Delight\Auth\Auth->processRememberDirective()
#2 /var/www/html/checkkdnr.php(18): Delight\Auth\Auth->__construct()
#3 {main}
2023-07-22 13:14:43 Creating new Cookie object with name: remember_6TpGq1xR_F05q3tke-JkBw
2023-07-22 13:14:43 Cookie saved. Value: ~, Expiry time: 1721582083, Path: /, Domain: 
2023-07-22 13:14:43 setRememberCookie() called. Selector: , Token: , Expires: 1721582083
2023-07-22 13:14:43 Stack trace: #0 /var/www/html/vendor/delight-im/auth/src/Auth.php(157): Delight\Auth\Auth->setRememberCookie()
#1 /var/www/html/vendor/delight-im/auth/src/Auth.php(69): Delight\Auth\Auth->processRememberDirective()
#2 /var/www/html/login.php(16): Delight\Auth\Auth->__construct()
#3 {main}
2023-07-22 13:14:43 Creating new Cookie object with name: remember_6TpGq1xR_F05q3tke-JkBw
2023-07-22 13:14:43 Cookie saved. Value: ~, Expiry time: 1721582083, Path: /, Domain: 

checkkdnr.php

<?php

header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://abc.example.net'); //the website where the tampermonkey script should add data from my application
header("Access-Control-Allow-Headers: Content-Type");
header("Access-Control-Allow-Credentials: true");

use Delight\Auth\Auth;

require_once 'vendor/autoload.php';
require_once '/var/www/include/php_db.inc.php';

$error = '';
$db = php_db("users");

// Create a new instance of the Auth class
$auth = new \Delight\Auth\Auth($db);
if (!$auth->isLoggedIn()) {
    // User is not logged in, redirect to the login page
    header('Location: login.php');
    exit;
}
ocram commented 1 year ago

Thanks a lot!

The behavior you showed happens only when an existing but invalid “remember me” cookie has been found, i.e. sent by the client and checked against the database on the server. So in your case there must have been a “remember me” cookie but it had invalid data. So then it is replaced with empty values (two empty strings separated by a ~) and written to the cookie with a duration of one year, to avoid unnecessary database requests on the subsequent page requests.

Do you have any idea where that invalid cookie initially comes from?

The file Auth.php with its processRememberDirective method is where you should add logging next, otherwise.

A possible solution would perhaps be replacing

$this->setRememberCookie('', '', \time() + 60 * 60 * 24 * 365.25);

with

$this->setRememberCookie(null, null, \time() - 3600);

and seeing if that changes anything. But that would only remove the invalid cookie instead of replacing it with a %7E cookie. The question is, why is the %7E cookie a problem in the first place? Why do logouts happen?