Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

daniel.schreij's avatar

How to refresh X/CSRF token after logout in SPA

I found some previous questions about this topic (such as https://laracasts.com/discuss/channels/general-discussion/tokenmismatchexception-with-logout) but they appear to be a bit dated and hard to relate to the current versions of Laravel and Vue (I'm currently working with L5.4 and V2.4), so I'm going to take the liberty to ask about this again.

I am working on a Vue-based SPA that uses vue-router for routing and interfaces with Laravel by making ajax calls to its API. All requests relating to login, logout, password resets, etc. are also handled through ajax. I use Laravel Passport for API authentication and Laravel's login scaffolding for user authentication. I have overwritten the methods that normaly return a view or redirect in the LoginController to make them return json reponses instead (i.e. authenticated() and logout()).

All goes well when logging into the application and working with it afterwards, but the trouble starts when the session expires or a user logs out. In both situations, the user is nicely redirected back to the login screen by vue-router, but any login attempts made afterwards result in a 500 error due to a TokenMismatchException.

This is of course to be expected, because Laravel provides the X-CSRF token to use in the head section of the HTML page, which is sent at the initial load of the application. After Vue and its router take over, pages are not refreshed anymore. So after logging out, a new token should be set, but due to the page not refreshing, the user is stuck with the old token which was initially loaded. If you then force a page refresh of the login page, everything indeed works again (as a new token has been set in the header with the refresh). Needless to say, page refreshes are undesirable in an SPA, especially when one wants to eventually convert it to a progressive web app.

What would be the best approach to tackle this problem? Send the new token (and maybe other important information that is normally refreshed after a page reload) explicitly with the logout response? But how would you then resolve this error when the TokenMismatchException occurs because the session has expired?

0 likes
8 replies
daniel.schreij's avatar

I'm a bit further and have partially solved this problem. Before showing the login form, I explicitly request a new CSRF token from the server in the created() method of the component. I then reassign the received token to the axios headers:

created(){
    axios.get('/refreshtokens')
        .then( response => {
            window.axios.defaults.headers.common['X-CSRF-TOKEN'] = response.data.csrfToken;
        })
        .catch( error => {
           /** handle error **/
        });
}

In the controller function for this endpoint , I simply return the CSRF token using Laravel's helper function:

public function refreshCsrfToken(){
    return ['csrfToken' => csrf_token()];
}

This solves the TokenMismatchException and allows the login process to proceed. The user is logged in, but the following API request to api/user to get the user info results in a 401 response:

{"error":"Unauthenticated."}

Directly trying again (by simply clicking the login button once more) does result in a succesful redirect to the user's dashboard (and thus an OK reply from /api/user)

My hunch is that the CSRF token needs to be refreshed again after the user has succesfully logged in, so I'm going to try that and alter this answer if I succeed (and report back if I don't ;) )

daniel.schreij's avatar

No luck...

My login function is as follows:

login(){
    // Copy the user object
    const data = {...this.user};
    // If remember is false, don't send the parameter to the server
    // This is necessary since Laravel checks simply if the remember
    // parameter is present in the request, and not if it's true or false
    if(data.remember === false){
        delete data.remember;
    }

    axios.post('/login', data)
        .then( response => {
            // Store the user data in local storage
            Vue.ls.set('user', response.data.user);
            // Store the newly created token in the axios headers, so it will
            // be automatically sent with each request made to the server.
            window.axios.defaults.headers.common['X-CSRF-TOKEN'] = response.data.csrfToken;
            // Redirect to the original url which was intended to be visited, 
            // or back to the main page if no URL was provided.
            let nextUrl = this.$route.query.redirect || '/';
            this.$router.replace(nextUrl);
        })
        .catch( error => {
            if(error.response.status == 422){
                this.errors = error.response.data;
            }else{
                /** Handle errors other than validation errors **/
            }
        });
}

But reassigning the CSRF token once more after a succesful login does not solve the problem that any following requests to the API are unauthenticated...

daniel.schreij's avatar

Solved the problem!

Apparently, Laravel does not directly create and return the new token after the user signs in, and there is a small delay here. I solved this by performing a separate refreshtokens request after I received a correct login response.

I don't know how secure this solution is, in the sense that you can retrieve the CSRF token simply by performing a GET request to /refreshtokens on the server, but considering that Laravel normally simply embeds the token into each page that it returns, this should be relatively harmless (am I right?).

This seems like quite a sloppy approach to me (as in, do I really need three round-trips to the server to get the correct CSRF token? Come on...), but here is my logic of my Vue login component nonetheless:

import axios from 'axios';
export default {
    data(){
        return {
            user: {
                email: '',
                password: '',
                remember: false
            },
            errors: {},
            passwordVisible: false,
            snackbar: false,
            snackbarMessage: '',
            snackbarContext: '',
        };
    },
    methods:{
        login(){
            // Copy the user object
            const data = {...this.user};
            // If remember is false, don't send the parameter to the server
            if(data.remember === false){
                delete data.remember;
            }

            axios.post('/login', data)
                .then( response => {
                    // Store the user data in local storage
                    Vue.ls.set('user', response.data.user);

                    this.refreshTokens()
                        .then( () => {
                            // Store the newly created token in the axios headers, so it will
                            // be automatically sent with each request made to the server.
                            window.axios.defaults.headers.common['X-CSRF-TOKEN'] = response.data.csrfToken;
                            // Redirect to the original url which was intended to be visited, 
                            // or back to the main page if no URL was provided.
                            this.$router.replace(this.$route.query.redirect || '/');
                        });
                })
                .catch( error => {
                    if(error.response.status == 422){
                        this.errors = error.response.data;
                    }else{
                        this.showErrorMessage(error.message);  
                    }
                });
        },
        refreshTokens(){
            return new Promise((resolve, reject) => {
                axios.get('/refreshtokens')
                    .then( response => {
                        window.axios.defaults.headers.common['X-CSRF-TOKEN'] = response.data.csrfToken;
                        resolve(response);
                    })
                    .catch( error => {
                        this.showErrorMessage(error.message);
                        reject(error);
                    });
            });
        },
        showErrorMessage(message){
            this.snackbarMessage = message;
            this.snackbarContext = 'error';
            this.snackbar = true;
        }
    },
    created(){
        this.refreshTokens();
    }
};
Snapey's avatar
Snapey
Best Answer
Level 122

I hate to tell you this, but csrf serves no practical purpose on a login page and can be safely ignored by adding it to the excluded routes in the middleware.

1 like
daniel.schreij's avatar

That makes me cry a little... ;)

At least I learned something by solving the problem the hard way (trying to get something positive out of this). Fixing this problem like you suggested. Thanks!

daniel.schreij's avatar

One more thing! It is necessary to refetch the token after a succesful login, right? Otherwise any following requests to the API will be made with an invalid token if I am correct?

So that makes that I only have to remove the created() method to discard the original token fetch, but all the other stuff is still necessary.

Snapey's avatar

I think the session is regenerated after a login, so yes, the token is anticipated to be different.

lorisleiva's avatar

Most developers tend to ignore CSRF vulnerability on login forms as they assume that CSRF would not be applicable on login forms because user is not authenticated at that stage. That assumption is false. CSRF vulnerability can still occur on login forms where the user is not authenticated, but the impact/risk view for it is quite different from the impact/risk view of a general CSRF vulnerability (when a user is authenticated).

Source

Please or to participate in this conversation.