Livewire: Session expired behaviour

Created on 19 Jan 2020  路  52Comments  路  Source: livewire/livewire

We get error 419 if the page is inactive for a while.

Describe the solution you'd like
I see that you have disabled session expiration before and re-enabled it, which was a good move. But can we create an api where we can handle this situation more gracefully? Maybe we decide to reload the page, or show a beautiful error, or something else.

Most helpful comment

For anyone looking for simple logout / session expiration protection, I just ended up overriding window.fetch to listen for 419 status responses and redirect the user to /login accordingly.

    window.fetch = (fetch => function() {
        return new Promise((resolve) => {
            return fetch.apply(this, arguments).then(response => {
                if (new URL(response.url).pathname.startsWith('/livewire/message') && response.status === 419) {
                    alert('Your session has expired.');
                    return window.location = '/login';
                }

                resolve(response);
            });
        });
    })(window.fetch);

That said, it would be really nice if Livewire had the ability to natively define interceptors in JS and / or PHP to make it easier to handle this sort of thing.

All 52 comments

If the session is expired, I would think the most elegant solution would be to redirect the browser to the login page. Reloading the current page should do that in most Laravel apps.

I think sometimes the csrf protection cookie is expired which causes the 419 error, but the user is still logged in. So, we probably shouldn't decide what happens. Just give the developer a way to handle the error how ever fits their application.

Yeah, this is definitely something I'm curious about addressing.

  • Offer a UI hook for devs to add their own "expired state"
  • Auto-redirect when an ajax request returns a 416
  • Auto-refresh csrf tokens

I don't know. I don't really like any of these options, but I agree this needs to be addressed. Would love to hear more thoughts.

I think there should be a callable/hook that lets the user decide what to do.

Perhaps the best default callable would be to auto refresh the csrf tokens.

I don't think the auto redirect is a good idea because it introduces another layer of ui for the dev to think about. ie. does the dev need to write and worry about redirected page, popup/modal, session flashed data, etc?

Maybe a config for enabling and disabling csrf token auto-refresh quickly. Then an event hook to handle all kinds of response codes (500, 419, ...) and the developer can decide what they wanna do.

If someone needs a quick fix they will just set a config. If that doesn't suit their needs, they probably need a low level control.

Oops, ran into this too. What鈥檚 wrong with auto-refreshing the CSRF token?

BTW Though I didn鈥檛 test it together with livewire, there is package doing this: https://github.com/GeneaLabs/laravel-caffeine

So, I used to have a little drip like laravel-caffeine to keep the session alive. But people pointed out that that poses a security risk and basically invalidates the concept of a session lifetime while a user has the site open.

Are we talking about two different things here?

Is a csrf-refresh drip different than a auto-session-refresh drip?

Sorry I haven't thought through this more deeply. Hoping someone who really understands the ins and outs of csrf to weigh in here.

I don't think the login session and the csrf lifetime is the same thing. And the lifetime of csrf token is only one layer of it's security. And we're not trying to disable it by default. It's best to provide a simple interface to decide what's best on a per project basis.

Maybe a config for enabling and disabling csrf token auto-refresh quickly. Then an event hook to handle all kinds of response codes (500, 419, ...) and the developer can decide what they wanna do.

Are you sure? It looks like the CSRF lifetime IS the same as the session:

image
(taken from vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php:179)

Yeah you're right. So, just the part where I said "we're not trying to disable it by default" 馃槃

How about

a config for enabling and disabling csrf token auto-refresh quickly. Then an event hook to handle all kinds of response codes (500, 419, ...) and the developer can decide what they wanna do.

And maybe some security advice about this and then we let the developer decide what's best for their use-case.

Is their a best practice solution for this? This is something I keep running into.

Hello folks... @calebporzio asked me yesterday to try adding a simple polling element to my app and see if that keeps everything "alive" beyond the amount of time the app env SESSION_LIFETIME is set to, and prevents the 419 errors...

So, this morning, on my local version of the app I set the SESSION_LIFETIME env variable to 1 minute.

First I wanted to make sure I was able to reproduce the 419 error based on this shortened SESSION_LIFETIME. So I waited a bit over a minute and tried to clicking a button with a wire:click attached to it. Bingo, the 419 error showed up.

Next I created a livewire component that would poll the server and output the date/time and set it to poll once per second.

A bit over a minute later I again clicked on a button that is tied to a wire:click and didn't get a 419 error. So, my initial thought was that this fixes everything!

But then I switched tabs and headed off to do something else in another browser tab. A few minutes later I came back to the app and was a bit surprised to see a 419 error without having done anything.

I believe that this is because as the livewire docs on polling say - https://laravel-livewire.com/docs/polling - "Livewire reduces polling when the browser tab is in the background so that it doesn't bog down the server with ajax requests unnecessarily."

So I don't know if there is a way to tell a polling request to ignore whether it is in a background tab or not? That might be a simple way to get around this issue.

For now I've set my app to have a SESSION_LIFETIME of 24 hours. Since its a project management app I close it at the end of the day and open it the following day. I think for my personal use case I should be good to go on that end for now, but I'd still kind of like to see something a bit more official as a "fix" or "workaround" for this. :)

Thanks!

@calebporzio tried the solution you suggested that @EldonYoder also tried. Same issue if browser focus is changed. Would a solution be something like wire:poll.forced or wire:poll.alive to keep the connect alive even if focus is changed? Tried to do this with alphine js but don't seem to have access to $dispatch inside of setInterval()

May not be a perfect solution but seems to be working well for me.

<div>
    <script>
        document.addEventListener('livewire:load', () => {
            setInterval(function(){ window.livewire.emit('alive'); }, 1800000);
        });
    </script>
</div>

Ah right, the polling interval slows when in the background. I recommend manually polling like shown if that鈥檚 the case. Closing for now.

The point of this issue was to add more control over how we wanna handle 419, not just extending it. This might not be optimal for some security reasons. We might wanna redirect the user to login page or show some message.

I'm wondering if we can just disable the popup (which blocks the entire page). It's weird to me that this is the behavior in production, why not fail silently if there's no way to gracefully handle this?

The popup is terrible for production, but failing silently is worse.
In order to have a usable UX you should either have a very simple error message that explains what has happened and what could be done about it, or redirect the user to login page.

In our case though, these are public pages - there's no need to be logged in anyway. We'd rather just have it fail silently or display a message in the component itself (rather than the entire page). Is @lightwalkernet solution still the best?

I guess that helps with your case. But if the user closes the laptop for example or puts pc to sleep and come back, I think you get the big modal again.

Right, why was this closed? @calebporzio can we reopen this?

@calebporzio I agree with most comments here that the solution you have now implicitly adopted may not be best for all cases, especially in contexts where security is a concern.

For me, an option to fail silently (and never attempt any further polling on that webpage afterwards) seems to be the easiest and fastest to implement rather than just hurriedly closing this issue (PR welcomed?).

If you will welcome PRs, we need to know the direction you want this to go before spinning up PRs, bearing in mind that most folks that will PR will most likely work on a solution that scratch their itch.

Kindly consider the suggestions made so far with respect to security concerns (which is the main problem with your currently accepted solution), so that we resolve this issue (even if temporarily).

Thanks for the awesome package!

This also happen to me in a public page without authentication.

image

In your handler.php @ render you can catch livewire exceptions and prevent the popup from opening:

if ($request->header('X-Livewire')) {
    return response()->json([
        'success' => false,
        'status' => $response->getStatusCode(),
        'error' => $exception->getMessage()
            ?: (Response::$statusTexts[$exception->getStatusCode()])
            ?? get_class_name($exception)
    ], $response->getStatusCode());
}

You obviously need to catch this json response to display the error to the user or it will still be a silent fail.

An example is adding a notifications component and converting the showHtml into an event, or forcing a reload with a 419 csrf error

<script>
  window.livewire.connection.driver.showHtmlModal = function (response) {
    const json = JSON.parse(response);
    if(json.status === 419){
      return location.reload();
    }
    window.livewire.emit('livewire-exception', json.error)
  };
</script>

With the reload method you will either get a new CSRF token and the user can re-submit, or they will be redirected to login if their login session & cookie has expired.

From a user perspective, this is pretty good, although not perfect. It would be nice if the original request could be re-sent instead of the re-load but I am not too sure how to achieve that.

From a security perspective, I don't believe it is too much different to a standard site.

Comments welcome!

This module https://github.com/GeneaLabs/laravel-caffeine seems to fix the issue in my case.

Drugs are temporary fixes my friend

This module https://github.com/GeneaLabs/laravel-caffeine seems to fix the issue in my case.

Thanks. I think this is the problem of Laravel, not Livewire.

This module https://github.com/GeneaLabs/laravel-caffeine seems to fix the issue in my case.

Thanks. I think this is the problem of Laravel, not Livewire.

No, this is a problem of Livewire.
As Livewire never updates the page, and then the CSFR, we have that error.

If you use Laravel without Livewire, you don't have that problem.
Another way yo fix this is to replace CSFR code with a Livewire component that auto refreshes.

In case of a token mismatch I am now redirecting the visitor to the home page (or login page), and it works fine so far. Instructions on how to achieve this are here: https://stackoverflow.com/a/57095026/4688612

Since the token mismatch only happens after a long time of inactivity, I think the general visitor will accept that he needs to re-login.

Here's the exact instructions:

So in your app\Exceptions\Handler.php file within the render method add this:

if ($exception instanceof \Illuminate\Session\TokenMismatchException) {
    return redirect()->route('login');
}

Now, everytime the token throws a mismatch exception, the visitor will be redirected to the login page.

Why was this thread closed? I tried auto refreshing workaround and that doesn't seem to work for me. I'm still waiting for an official solution...

In case of a token mismatch I am now redirecting the visitor to the home page (or login page), and it works fine so far. Instructions on how to achieve this are here: https://stackoverflow.com/a/57095026/4688612

Since the token mismatch only happens after a long time of inactivity, I think the general visitor will accept that he needs to re-login.

Yes, but that will help just on webs where the visitors are not guests

This isn't specific to livewire. This is something that happens in any laravel app with a session set. When the session times out and then a post request is made, the csrf tokens will not match and you'll get a 419. If it's not a security concern for your app, the extend the session to long time like 6 months. If it is, catch the exception and handle it. Refreshing the page will work in that case.

@devcircus
I already answered that

This module https://github.com/GeneaLabs/laravel-caffeine seems to fix the issue in my case.

Thanks. I think this is the problem of Laravel, not Livewire.

No, this is a problem of Livewire.
As Livewire never updates the page, and then the CSFR, we have that error.

If you use Laravel without Livewire, you don't have that problem.
Another way yo fix this is to replace CSFR code with a Livewire component that auto refreshes.

Spin up a new laravel app, let the session expire then do a post request without refreshing or doing a full round trip to the server. You get the same thing. I have a code snippet in vscode just for that situation because I have to handle it in most apps.

Spin up a new laravel app, let the session expire then do a post request without refreshing or doing a full round trip to the server. You get the same thing. I have a code snippet in vscode just for that situation because I have to handle it in most apps.

This is what I want to tell. The 419 comes from Laravel request life cycle when the session expired, not from Livewire.

The below quick fix just try to keep the session alive, like you refesh the page frequently.

This module https://github.com/GeneaLabs/laravel-caffeine seems to fix the issue in my case.

If someone doesn't use Livewire, they also get the 419 in some case, for example, the login form. When you open the login page and leave it a day (or when the session get expired), you must refresh the page before can do login.

@devcircus @cosmospham You're missing the point. Nobody is asking why we're getting 419. We want to have more control over how to handle exceptions like this in production.

It is really unfair that @calebporzio is just silent about this very important issue. Even if we are to do a PR as a community and persons of interest, he is too quiet about this issue as though it is a minor issue. He even closed this issue!??

The bone of contention is that there is a feature of the package that causes certain undesired behavior. It is supposed to "refresh the component" at specified intervals. Anything outside this contract/advertisement is an error, and I don't think all these workarounds are indeed actually "solutions". Polling is a feature of LW, let's make it perfect and usable for most use-cases as advertised!

A lot of suggestions have been made. Why is the author uninterested in resolving this issue or supporting one of the suggestion so that a PR can be worked-out.

To put things in context, I will summarize below the suggestions that have been made (note suggestions, not workarounds):

  1. Offer a UI hook for devs to add their own "expired state"
  2. Auto-redirect when an ajax request returns a 416
  3. Auto-refresh csrf tokens
  4. Introduce a new directive like wire:poll.forced or wire:poll.alive that polls continuously, irrespective of whether browser focused or not

The pros and cons of these suggestions have been discussed too.

Finally, I think this polling feature of LW as implemented currently, should be supplemented with some kind of workarounds described on this page, or you should simply refactor your code such that it uses more reliable feature of the package (e.g. refactor to use events instead of polling), more so that the author has closed this issue, and he is silent about the willingness to address the way polling is currently implemented, or which of the suggested solutions he is willing to accept PRs for.

Peace out :v:

It is really unfair that @calebporzio is just silent about this very important issue. Even if we are to do a PR as a community and persons of interest, he is too quiet about this issue as though it is a minor issue. He even _closed_ this issue!??

The bone of contention is that there is a feature of the package that causes certain undesired behavior. It is supposed to "refresh the component" at specified intervals. Anything outside this contract/advertisement is an error, and I don't think all these workarounds are indeed actually "solutions". Polling is a feature of LW, let's make it perfect and usable for most use-cases as advertised!

A lot of suggestions have been made. Why is the author uninterested in resolving this issue or supporting one of the suggestion so that a PR can be worked-out.

To put things in context, I will summarize below the suggestions that have been made (note _suggestions_, not _workarounds_):

  1. Offer a UI hook for devs to add their own "expired state"
  2. Auto-redirect when an ajax request returns a 416
  3. Auto-refresh csrf tokens
  4. Introduce a new directive like wire:poll.forced or wire:poll.alive that polls continuously, irrespective of whether browser focused or not

The pros and cons of these suggestions have been discussed too.

Finally, I think this polling feature of LW as implemented currently, should be supplemented with some kind of workarounds described on this page, or you should simply refactor your code such that it uses more reliable feature of the package (e.g. refactor to use events instead of polling), more so that the author has closed this issue, and he is silent about the willingness to address the way polling is currently implemented, or which of the suggested solutions he is willing to accept PRs for.

Peace out 鉁岋笍

No, he is not silent https://github.com/livewire/livewire/discussions/1031#discussioncomment-23026

It is really unfair that @calebporzio is just silent about this very important issue. Even if we are to do a PR as a community and persons of interest, he is too quiet about this issue as though it is a minor issue. He even _closed_ this issue!??
The bone of contention is that there is a feature of the package that causes certain undesired behavior. It is supposed to "refresh the component" at specified intervals. Anything outside this contract/advertisement is an error, and I don't think all these workarounds are indeed actually "solutions". Polling is a feature of LW, let's make it perfect and usable for most use-cases as advertised!
A lot of suggestions have been made. Why is the author uninterested in resolving this issue or supporting one of the suggestion so that a PR can be worked-out.
To put things in context, I will summarize below the suggestions that have been made (note _suggestions_, not _workarounds_):

  1. Offer a UI hook for devs to add their own "expired state"
  2. Auto-redirect when an ajax request returns a 416
  3. Auto-refresh csrf tokens
  4. Introduce a new directive like wire:poll.forced or wire:poll.alive that polls continuously, irrespective of whether browser focused or not

The pros and cons of these suggestions have been discussed too.
Finally, I think this polling feature of LW as implemented currently, should be supplemented with some kind of workarounds described on this page, or you should simply refactor your code such that it uses more reliable feature of the package (e.g. refactor to use events instead of polling), more so that the author has closed this issue, and he is silent about the willingness to address the way polling is currently implemented, or which of the suggested solutions he is willing to accept PRs for.
Peace out v

No, he is not silent #1031 (comment)

Strange why @calebporzio replied without referencing this earlier issue. I think they are related and should be merged, more so that this ongoing (albeit "closed") issue has already gained more traction than the new thread being formed at #1031

Either ways, I am glad that there's still hope with this

There's a legitimate reason to allow session expiration which, in turn, invalidates the csrf token. Laravel purposefully throws a 419 and leaves this up to the user to handle. I don't want a framework to ignore expired sessions or automatically refresh/extend it. There could be security concerns involved.

The default behavior for Laravel is to throw a 419 when a post request is submitted after the session expires. This happens in all laravel apps.

IMO, The only possible role for livewire here, would be to maybe provide a slightly more convenient way for the developer to decide how to handle it, and document the default behavior regarding the 419.

I think a redirection to login page, or refreshing the login page is the best.

I have an estrange behavior with livewire... whenever I go back to a livewire component, session is expired and it won't work anymore. This is horrible in production mode since no error is shown.
Also in topic, my livewire csrf token expires too soon, way earlier from default lavarel session timeout.

Here is a simple solution to 419 problem:
https://gist.github.com/tanthammar/88c7a28b68573f70dc3d09abb26d454b

BUT it only works if the window is in focus!

If the user has multiple tabs open, and browses away from this one, the session expires any way. I can see in the console that the script keeps polling the server but I do not know why the session is terminated.

Suggestions?

For anyone looking for simple logout / session expiration protection, I just ended up overriding window.fetch to listen for 419 status responses and redirect the user to /login accordingly.

    window.fetch = (fetch => function() {
        return new Promise((resolve) => {
            return fetch.apply(this, arguments).then(response => {
                if (new URL(response.url).pathname.startsWith('/livewire/message') && response.status === 419) {
                    alert('Your session has expired.');
                    return window.location = '/login';
                }

                resolve(response);
            });
        });
    })(window.fetch);

That said, it would be really nice if Livewire had the ability to natively define interceptors in JS and / or PHP to make it easier to handle this sort of thing.

Made a Livewire, Turbolinks Blade component. (Gist)
One should be placed in the <head> and the other after @livewireScripts

  1. Option to keep session alive permanently. (has been tested with sleeping computer, macosx)
  2. Resets on Turbolinks and Livewire loaded events
  3. Alerts the User 10 minutes before session is ending
  4. Does not poll the server if the window is not in focus, (can be changed)
  5. If the window has been out of focus it checks if the session is active, else redirects to login
  6. Redirects to login if the session has expired.
  7. Uses config('session.lifetime') for the session timer

<x-session-timeout-alert-head :permanent="false" /> <x-session-timeout-alert-after-livewire-scripts />

https://gist.github.com/tanthammar/02c615e73022c44dda6533ed9416ac29

I made a middleware handle this 419. It will redirect the page to login page whenever users submit an expired form (expired CSRF token), or return window.location in script tag with a Livewire request with expired token.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;

class Check419 {
    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next) {
        $response = $next($request);

        $acceptable = $request->getAcceptableContentTypes();

        if ($response->status() === 419) {
            Auth::logout();
            Session::flush();

            if (isset($acceptable[0]) && Str::contains($acceptable[0], ['/json', '/html', '+xml'])) {
                $response->setContent('<script>window.parent.location="/login"</script>');
                return $response;
            }

            return redirect()->to('/login');
        }

        return $response;
    }
}

And put it at the top:

class Kernel extends HttpKernel
{

    protected $middleware = [
        \App\Http\Middleware\Check419::class,
        // ....
    ];
// ...

I made a middleware handle this 419. It will redirect the page to login page whenever users submit an expired form (expired CSRF token), or return window.location in script tag with a Livewire request with expired token.

The problem I have with this idea is that the user only sees this when they start typing into the form (with .lazy, its when they tab to the next field). Imagine this is a login form, they come back to the site and start to login, they key their email move to the next field and blam, the page refreshes and they are looking at an empty login form again.

Yes, their session expired whilst they were on the login page (overnight for instance), but they cannot login without this happening EVERY time they login.

Solutions for this are,

  1. Never leave the user on a login form after logout.
  2. Never redirect the user to login form after session expiry.
  3. Add <meta http-equiv="refresh" content="{{ config('session.lifetime') * 60 }}"> to the head of the page

This last option works for regular pages, but for livewire enabled pages the page will refresh whilst the user is happily interacting with the Livewire component - so this is not an option here.

Regarding earlier comments of session lifetime and csrf token lifetime being the same thing - they are not the same. A user can maintain their session in one tab, meanwhile the csrf token in another tab has gone stale. They are happily working on one tab, switch to the other start to fill a form and then when they tab to a different field 'blam' white screen with 419 error.

Redirecting to login in this case is not the option. They _are_ logged in. What I want is a way for Livewire to silently get a new csrf token and repost the data - provided the user has a valid session of course.

I updated Livewire today, and now I get an alert box...

"This page has expired due to inactivity. Would you like to refresh the page?"

  1. Never leave the user on a login form after logout.
  2. Never redirect the user to login form after session expiry.

How about some users just open the login form and they leave there?

I updated Livewire today, and now I get an alert box...

"This page has expired due to inactivity. Would you like to refresh the page?"

How to change this message?

@santyzu13 its hardcoded in english I'm afraid

@santyzu13 its hardcoded in english I'm afraid

Thanks @snapey

I found a solution: https://github.com/livewire/livewire/pull/1146

Was this page helpful?
0 / 5 - 0 ratings