Don't use a session. Use JWT in your SPA.
Handling session expiry in a Vue SPA with Laravel backend.
I need to handle situations when user session expires on the front-end. I'll describe how I do this now, please tell me if you have a better solution.
When session expires Laravel by default returns a 500 response representing a TokenMismatchException. I decided to overwrite the handler to return a 420 response(or whatever arbitrary code, not to conflict with other returned by the app) and in the response body a new CSRF token created with session()->token(). Here's the handle method of the VerifyCsrfToken middleware:
public function handle($request, Closure $next)
{
try {
return parent::handle($request, $next);
} catch(TokenMismatchException $exception) {
return new Response([
'newToken' => session()->token()
], 420);
}
}
It's all great now. I make a POST request, it returns 420, I know I need to reauthenticate the user so this is what I did: I created an axios interceptor to catch these errors:
window.axios.interceptors.response.use(response => {
// Everything fine, just pass it for further processing.
return response;
}, error => {
// If it is an HTTP error
if(error.response) {
if(error.response.status == 420) { // Session expired.
// Set the new token as default HTTP header.
axios.defaults.headers.common['X-CSRF-TOKEN'] = error.response.data.newToken;
// If the user was signed in, we need to prompt for password.
if(app.signedIn) {
return new Promise((resolve, reject) => {
// Request the password from user.
app.requestPassword()
.then(() => {
// If the user succeeds with authentication
// Repeat the same request with the new token and return the response to the original requesting function.
let request = error.response.config;
request.headers['X-CSRF-TOKEN'] = error.response.data.newToken;
axios.request(request).then(response => {
resolve(response)
}, error => {
reject(error)
})
})
});
}
} else {
// Handle other errors
}
}
return Promise.reject(error);
});
The app.requestPassword() looks like this:
requestPassword() {
// Necessary to show the reauthentication modal.
// It will be set back to false once the authentication succeeds.
this.sessionExpired = true;
return new Promise((resolve, reject) => {
app.$on('reauthenticated', (success) => {
if(success) {
resolve();
} else {
reject();
}
})
})
}
Setting the sessionExpired to true shows a modal saying "Your session has expired. Please provide your password again". If the user provides correct password, the modal closes and the reauthenticated event is raised with success = true. If the user closes the modal manually without reauthenticating the same event is raised but with success = false and he gets redirected to login page.
Here's the typical request handling workflow:

It all looks and works great but I think it's more complicated then it should be. I end up creating 4 nested promises and I think there might be a better way to handle this.
Have you got any experience with solving this problem? I'd appreciate any hints, articles etc.
Please or to participate in this conversation.