Search code examples
phpsessioncross-domainsession-cookiesapple-sign-in

Losing PHP Session Cookie in Callback from Sign in with Apple on Web


I'm running into the same issue as @whip from last year (Session is reset after redirect from Apple) with my PHP configuration trying to implement "Sign in with Apple" into my website. I fully expect that my issues stem from a lack of understanding around how the browser and PHP handle session cookies. Hopefully, with a more detailed description of my configuration and the events taking place, the underlying issue can be found and help someone else in the future.

Example code and documentation for Sign in with Apple implementation is coming from this blog post. It is one of the few examples that walks through both the setup of the necessary Apple client and service IDs, but also shows token decoding examples. (https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple)

As you can see in the example, there is a temporary random state variable that is saved into a PHP session. This is used to validate the returning POST from Apple to ensure that the correct response session is being addressed. In my case, however, the redirection POST request from Apple is causing (in most cases, see below) a new PHPSESSID cookie to be created and the previously-saved state variable is being lost.

Here is what I am currently experiencing in the authentication flow:

  1. Session is started as per the common framework init (see below)
  2. Random verification state variable created and saved into session variable:
$_SESSION['state'] = bin2hex(random_bytes(5));
  1. Link is created for Sign in with Apple button as per documentation SIGN IN WITH APPLE BUTTON
  2. Checking the browser (Chrome) inspector's Application tab, you can see that there is a PHPSESSID cookie managing the current session. In this case it has ID tk4bh... BROWSER INSPECTOR COOKIE SESSION ID
  3. Click on the link, user is taken from my website to appleid.apple.com to start auth loop APPLE ACCOUNT SIGN IN
  4. Complete login with Apple and click button to return to my website with POST containing auth token COMPLETE APPLE ACCOUNT SIGN IN
  5. The POST will route back to whatever the provided Redirect URL was provided in the initial Sign in with Apple link (Note: this URL needs to be added to the Apple Service ID used for the client)
  6. Once the POST request returns to my website, you will see error messaging that the request has failed to authenticate because the State variable provided by the return POST payload does not match the saved $_SESSION['state'] variable. This is because the $_SESSION['state'] value is not set.
  7. Again, checking the browser inspector's Application tab, you can see that there is a PHPSESSID cookie present, but that it is a new cookie. The ID for this new cookie is now rf1k5... NEW SESSION ID IN BROWSER INSPECTOR

Interestingly, however, if you add a step to the previous process and start with a completely fresh cookie, then go through the process, the cookie is retained after the return POST request from Apple and the authentication loop can be completed. So you right click on the website's name from within the browser inspector's Applications tab and choose Clear. Then refresh the login page to save the State session variable, click on the Sign in with Apple button, complete the process and return to the page to find a successful login.

So the question remains, why is a newly-created cookie able to persist in the browser through the Apple authorization look and redirection, but a cookie that has been created at some other point in the user's browsing (every page shares the same session_start method) and then used for saving the validation state variable consistently gets "forgotten" and a new cookie has to be created?


Website is running nginx and PHP 8.1.29. Current session INI config as follows:

PHP SESSION CONFIGURATION

NOTE:

  • session.cookie_lifetime is set to 0 (which I would like to preserve)
  • session.save_path is to a Redis server (which shouldn't affect anything in the browser client side, but ??)
  • All sessions are initiated from a single call in the PHP framework that adds the following session cookie options:
//---------------------------------------------------------
// safe wrapper around `session_start` to ensure session id is valid, avoiding PHP warnings/errors
function safe_session_start() {
    // if session has already started, just return
    if ( session_status() != PHP_SESSION_NONE ) {
        return true;
    }

    if ( isset($_COOKIE[session_name()]) AND ! preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[session_name()]) ) {
        unset($_COOKIE[session_name()]);
    }

    $options = ['cookie_secure' => COOKIE_SSL, 'cookie_httponly' => true, 'cookie_domain' => COOKIE_DOMAIN, 'cookie_path' => COOKIE_PATH];
    return session_start($options);
}
//---------------------------------------------------------

Solution

  • You are probably experiencing the new cookies SameSite security default settings in the browsers.

    https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value

    If the cookie SameSite is not saved as none, it will not be loaded by the Apple callback, which will be generating a brand new session when hitting the callback endpoint.

    When using Laravel, you can change this behaviour in session.php

    'secure' => true, //only https
    'same_site' => 'none' //allow cross-site cookie requests
    
    

    so as long as the site is on https, the original cookie and therefore the existing session will be preserved.

    Another option would be caching your session before going to the Apple auth screen like

    $sessionData = session()->all();
    $token = Str::random(40); //or use a uuid, csrf_token() ...
    Cache::put($token, $sessionData, now()->addMinutes(30)); 
    
    

    Send $token as the "nonce" value to Apple, then retrieve it in the callback

    $sessionData = Cache::get($token); //from nonce value in Apple callback
    
        if ($sessionData) {
            session()->flush();
            session()->put($sessionData);
            Cache::forget($token);
        }
    
    

    This way, you temporarily store the session data securely and re-establish it after the user logs in with Apple, without relying on cookies.