Search code examples
phpsession-variablesgoogle-signin

PHP Session discrepancy between development (localhost) and hosted service


I'm implementing a google sign-in feature which uses PHP sessions across pages to determine whether the user is in fact signed in. This works perfectly on my local machine. When I upload to my hosted server (happens to be Google Cloud) and adjust the client ids accordingly and then sign in as normal, the sign-in process completes, but the user is reported as being not signed in. A few page refreshes and / or clicks on the "sign in" button then result in the user being recognised as being signed in. I am assuming that the session variables are not being set correctly or that there is some delay in setting them. However, maybe there is another issue that I'm unaware of (again there are no problems when I run the service locally). I appreciate that this is a bit of a vague question. I have tried using session_write_close() in case the sessions were being kept open for too long, though this made no discernible difference.

Once the user is successfully authenticated by google sign-in, a POST page, oauth.php reads the variables and writes them to the $_SESSION variables, for example:

session_start();
...
$_SESSION["auth"] = true;
$_SESSION["userId"] = $row['id']; // A SQL query and further logic either populates this field (existing user) or leaves it blank (new user)

Upon completion, the server then loads loggedin.php which determines if this is a new user or an existing user and the either loads the new user form, or the main page respectively:

session_start();
if (!$_SESSION['auth']) {
    print("     You must be signed in to edit your profile.");
} else {
    if ($_SESSION['userId']) { // If userId is set, existing user
        header('Location: index.php');
    } else { // userId is not set, new user
        header('Location: profileedit.php');
    }
}

Any suggestions for what else I can look at? The solution works perfectly on my local machine, just not well when I upload and host it.

Further to the comments below,

session_id() is consistent from index.php to login.php to oauth.php to profile.php on my local machine. When I trawled through the error_log on the hosted server though, I see the following error message:

[Sun Oct 17 17:24:32.094053 2021] [php7:error] [pid 25817] [client XXX:51629] PHP Fatal error:  Uncaught Firebase\\JWT\\BeforeValidException: Cannot handle token 
prior to 2021-10-17T17:24:54+0000 in /var/www/html/vendor/firebase/php-jwt/src/JWT.php:142\nStack trace:\n#0 /var/www/html/vendor/google/apiclient/src/AccessToken/Verify.php
(106): Firebase\\JWT\\JWT::decode()\n#1 /var/www/html/vendor/google/apiclient/src/Client.php(793): Google\\AccessToken\\Verify->verifyIdToken()\n#2 /var/www/html/includes/oa
uth.php(18): Google\\Client->verifyIdToken()\n#3 {main}\n  thrown in /var/www/html/vendor/firebase/php-jwt/src/JWT.php on line 142, referer: http://XXX/login.
php

The error was logged at 17:24:32 and the token appears to be set to be valid from 17:24:54. Could it simply be that there is a time sync issue between Google's auth server and the hosted web server (also Google Cloud, incidentally)? I also noted in the error_log that the $_SESSION['auth'] is "undefined index", which suggests that the authentication isn't happening at all - although after a few refreshes, the authentication does then work, which might support the time sync issue. Not quite sure what I can do about this though!

Further update. It does look like the issue is due to a time discrepancy. In fact, in the google API library vendor/firebase/php-jwt/src/JWT.php there is the following comment and code:

    /**
     * When checking nbf, iat or expiration times,
     * we want to provide some extra leeway time to
     * account for clock skew.
     */
    public static $leeway = 1;

I attempted to override this in my code using:

$jwt = new \Firebase\JWT\JWT; //Allow for discrepancies between server and auth times
$jwt::$leeway = 5;

But that didn't work. So then I edited the code in vendor/firebase/php-jwt/src/JWT.php directly:

    /**
     * When checking nbf, iat or expiration times,
     * we want to provide some extra leeway time to
     * account for clock skew.
     */
    public static $leeway = 5;

I have also edited the vendor/google/apiclient/src/AccessToken/Verify.php as follows:

//      if (property_exists($jwtClass, 'leeway') && $jwtClass::$leeway < 1) { // Original code. Remove the $leeway<1 constraint as the $leeway would not be < 1
    if (property_exists($jwtClass, 'leeway')) {
      // Ensures JWT leeway is at least 1
      // @see https://github.com/google/google-api-php-client/issues/827
      $jwtClass::$leeway = 5;
    }

But this still doesn't work. I'm still stumped!


Solution

  • Crikey! That took some time to figure out. The unit for $leeway is in seconds. Increasing this value by 5 (as recommended) had no effect for me as the clock on my webserver is 24 seconds slower than the auth server. I was able to deduce this by adding the following error logging to vendor/firebase/php-jwt/src/JWT.php:

            // Check the nbf if it is defined. This is the time that the
            // token can actually be used. If it's not yet that time, abort.
            if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
                throw new BeforeValidException(
                    'Cannot handle token prior to (nbf) ' . \date(DateTime::ISO8601, $payload->nbf) . ' Current time ' . \date(DateTime::ISO8601, $timestamp) . ' leeway ' . \date(DateTime::ISO8601, $tim
    estamp + static::$leeway)
                );
            }
            // Check that this token has been created before 'now'. This prevents
            // using tokens that have been created for later use (and haven't
            // correctly used the nbf claim).
            if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
                throw new BeforeValidException(
                    'Cannot handle token prior to (iat) ' . \date(DateTime::ISO8601, $payload->iat) . ' Current time ' . \date(DateTime::ISO8601, $timestamp) . ' leeway ' . \date(DateTime::ISO8601, $tim
    estamp + static::$leeway)
                );
            }
            // Check if this token has expired.
            if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
                throw new ExpiredException('Expired token');
            }
    

    This resulted in the following error.log output:

    [Sun Oct 17 23:01:59.120593 2021] [php7:notice] [pid 25837] [client XXX:53053] oauth.php JWT server time mismatch. Retry attempt: 9 Error: Firebase\\JWT\\BeforeValidException: Cannot handl
    e token prior to (iat) 2021-10-17T23:02:23+0000 Current time 2021-10-17T23:01:59+0000 leeway 2021-10-17T23:02:04+0000 in /var/www/html/vendor/firebase/php-jwt/src/JWT.php:142\nStack trace:\n#0 /var/
    www/html/vendor/google/apiclient/src/AccessToken/Verify.php(106): Firebase\\JWT\\JWT::decode()\n#1 /var/www/html/vendor/google/apiclient/src/Client.php(793): Google\\AccessToken\\Verify->verifyIdTok
    en()\n#2 /var/www/html/includes/oauth.php(24): Google\\Client->verifyIdToken()\n#3 {main}, referer: http://XXX/login.php
    

    Originally I set the value of $leeway to 5 (as recommended) and somehow determined that this value wasn't being read. I then set about editing the vendor code directly. Having completed this journey, I have proven that the following code within my includes/oauth.php script does in fact have the desired effect of increasing the $leeway property.

    $jwt = new \Firebase\JWT\JWT; //Allow for discrepancies between server and auth times
    $jwt::$leeway = 100;
    

    If you're looking for the answer, that's it above. For completeness, I uncovered the following on my journey...

    The facility to set $leeway in vendor/firebase/php-jwt/src/JWT.php exists but is not actually used in my implementation (perhaps because I'm using google sign-in, and not firebase)

       /**
         * When checking nbf, iat or expiration times,
         * we want to provide some extra leeway time to
         * account for clock skew.
         */
        public static $leeway = 100;
    

    So instead, I set leeway in vendor/google/apiclient/src/AccessToken/Verify.php. Also, frustratingly, the original code ignores setting the value entirely as the property doesn't exist, so I added an else clause to set the property anyway:

          if (property_exists($jwtClass, 'leeway') && $jwtClass::$leeway < 1) {
          // Ensures JWT leeway is at least 1
          // @see https://github.com/google/google-api-php-client/issues/827
          $jwtClass::$leeway = 100;
          error_log("Property exists. Updating leeway", 0);
        } else {
          $jwtClass::$leeway = 100;
          error_log("Property does not exist. Updating leeway", 0);
        }