Laravel Session Problems
I have recently spent a few hours trying to diagnose and fix a rather annoying problem with session persistence using cookies in Laravel, it's an interesting problem so I am recording it here because I am bound to come up against it in the future.
Symptoms
We were building a new feature for Vestd to allow users to add links to other social profiles but in a verified way, this meant using Laravel Socialite to allow users to connect via oauth, if this was successful we would store the url for their profile.
This is a pretty simple proposition and it was built fairly quickly, unfortunitly when returning from the 3rd party the process the process would occasionally fail with a InvalidStateException
.
This exception is thrown by Socialite when it tries to match a state parameter it has stored locally with the one returned by the oauth provider. The aim of this check is to ensure the process was completed in one go rather than someone hitting the callback url separately or otherwise going through the process as it wasn't intended.
Storing the state in session
When you start of the oauth process a random string is generated, this is stored in the local session and then included in the url as the user is redirected off site to continue the oauth process. When the user is redirected back the token is returned in the url and then matched with the one stored locally, if they match all is good and the oauth process is completed if they don't match an InvalidStateException
is thrown.
What was going wrong
On the site is a regular 6 second AJAX call to fetch any new notifications for the user (this is due to be replaced with a permanent websocket connection), the call is made to the same back end and contains the same session data stored in the users cookie. The current "state" is stored in the users cookie and maintains the login as well as other bits and pieces, nothing is stored on the server and each web request is independent from each other.
If the user clicked the oauth connect button at roughly the same time as one of the background notification updates the two requests would head off with the same session data and two separate processes would run on the server to handle these. The oauth process would do its thing and set a state parameter in the session and as part of the redirect return this to the user as a replacement to the local session cookie, this is all good and what we would expect.
The other notification request which was being processed alongside is now ready and gets returned to the browser but a fraction of a second after the other one, this request also contains the users session as a replacement to the local cookie but because it is an independent request it has no knowledge of the session changes the other request just made.
The browser receives this second request and updates the session cookie once again but this time it replaces the one that contains the state parameter with the one that doesn't, all this happens in the moment before the redirect kicks off and sends the user offsite.
When the user returns the locally stored session (the one without the state value) is sent to the server along with the oauth callback, it then tries to extract the local state value and can't because that version of the session was overridden.
This error was affecting us because of the background ajax request but it could be triggered by other events, any other server request running alongside that has a response returned from laravel will potentially cause this error.
The screenshot of the network timeline below highlights the problem with the two requests
A solution?
Unfortunately there is no easy fix. The only reliable way that I am aware of is to use session locking, I am not sure how this can work with session cookies but for example if the session data was stored on the server in a database one request would start using the session and lock it to prevent any other requests from using it, this would prevent it getting overridden.
Laravel doesn't support session locking so this isn't something we could turn on even if we did switch to a different method of session storage.
The solution I ended up going with was to turn off session handling for requests that don't need it. The notification ajax request doesn't make any updates to the session so the returned cookies can be removed meaning nothing gets overridden, the same applies to requests for assets or potentially all GET requests although this will depend on how the application works.
A laravel fix
Disabling the session in laravel is sadly a bit of a hack, you need to change the session type to an array meaning nothing will be saved between requests. I had done this before when I wanted to reduce the side of an http response, in that case I used url detection and changed the session type but this time I came up with a simple piece of middleware that can be applied to routes as and when it's required.
class NoSession
{
public function handle($request, Closure $next)
{
app('config')->set('session.driver', 'array');
return $next($request);
}
}
I set this up in the http kernel as a piece of route middleware and simply applied it to any route that definitely didn't need to update the session. This doesn't solve the problem, it just reduces the chances of it happening which right now is the best we can do.