GoTrueJS "Remember Me" functionality

I’ve been working with the GoTrue library and Netlify Identity with a lot of success and I have a quick question.

The “setCookie” parameter when initiating instance of GoTrue and “remember” parameter to login indicate that the GoTrue library supports “Remember Me” functionality like most authentication systems (and the Identity widget seems to offer a Remember Me parameter), however the Github issue here (jwt() method does not update the token currentUser() · Issue #71 · netlify/gotrue-js · GitHub) indicates this functionality may not be working, and I have not been able to figure it out how to renew an expired token or make “Remember Me” work on my own.

Can I get an explicit confirmation that this is functionality the GoTrue library doesn’t support, or am I simply not finding the documentation how to do it properly? How would one renew a token? Even if GoTrueJs functionality for this is broken is this still supported by GoTrue in backend so it can be implemented on front end from scratch or does GoTrue simply not provide token renewal functionality?

Kind Regards,
Carl

Hey Carl,

Thanks for bearing with us.

I’ve had a dig through some issues and use cases. I’ve managed to find this issue which goes in to a bit of detail regarding how we can check and refresh the token. This is the code you’ll be looking for.

So, in short – it has the functionality!

Thanks for following up. I developed a work around that works for me that I will explain for future reference, but unfortunately the solution you provided will not really work.

When the GoTrue JS library is instantiated it creates an internal store for holding the access and refresh tokens. You are correct that the jwt function will use the refresh token to provide a new access token if it is expired, however the function fails to update the instance’s internal store so the old refresh token and old access token are used on subsequent calls of jwt, which will error because refresh tokens can’t be used more than once. The known bug here (jwt() method does not update the token currentUser() · Issue #71 · netlify/gotrue-js · GitHub) is actually more substantial than the report indicates because not only does it fail to update the access token but it fails to update the refresh token so once the jwt function actually uses the refresh token it will never work again until the user signs out and logs back in again.

To actually keep users logged, an application must maintain the state of the access token and refresh token independently of the GoTrue instance. Unfortunately the jwt function only returns the new access token and not the new refresh token so to actually maintain the state of the refresh token you will have to eschew the GoTrueJS library entirely and make fetch requests to the token endpoint yourself. Here is an example of a Vuex Action I use to validate the current token and update the token store if it is expired:

validate({commit, state, dispatch}) {
        if (!state.currentUser) return Promise.resolve(null);
        const user = state.currentUser;
        if (user.token.expires_at <= Date.now()) {
            // keep users logged in
            const formData = new FormData()
            formData.append('grant_type', 'refresh_token')
            formData.append('refresh_token', user.token.refresh_token)
            fetch('/.netlify/identity/token', {
                method : "POST",
                body : formData
            }).then(x=>x.json()).then(newToken => {
                user.token.access_token=newToken.access_token
                user.token.refresh_token=newToken.refresh_token
                user.token.expires_at = jwt_decode(newToken.access_token).exp * 1000;
                commit("SET_CURRENT_USER", user);
            })
            return null
        }
        return null;
    }

I talked with several folks at Netlify who say this isn’t a critical issue and that GoTrueJS is not a fully supported product, which is fine, and I found my workaround so I am satisfied, but I feel very strongly that the bug in the GoTrueJS library is worth fixing, and documenting the refresh token process better would be better for everyone.

5 Likes

Hi Carl,

First off, thank you so much for your explanation and implementation. I’ve been having very similar problems with my site from day 1 of using GoTrue, and could never figure out what the underlying issue was until now. What you described is exactly what I’m experiencing, and this solution is what I needed to fix that, so thank you!!

Now that I have a working implementation (in vanilla js), I find that my manually retrieved refresh_token isn’t kept on refresh or switching tabs and was planning on using a cookie to store that data. Do you foresee any issue with this, or perhaps a better implementation?

That is pretty much exactly what I am doing using these functions:

function saveState(key, state) {
    window.localStorage.setItem(key, JSON.stringify(state));
}
function getSavedState(key) {
    return JSON.parse(window.localStorage.getItem(key));
}

Which are essentially copy and pasted from gotruejs-in-vue/src/state/modules/auth at master · shortdiv/gotruejs-in-vue · GitHub

So using cookies should work perfectly fine

1 Like

Gotcha - one weird behavior I noticed was that my cookie ‘nf_jwt’ wasn’t being overwritten when refreshing the token. Because of this, functions and redirects weren’t working properly as the cookie was storing an older, invalid access_token. It wasn’t until I actually deleted the cookie in my refresh function and set it again that things started working properly. I say this is weird because the cookies to store refresh_token and the expiration date were updated consistently - somehow just the nf_jwt cookie was being left behind. Not sure if you’ve experienced this, or understand why it’s happening, but I thought I’d post here in case anyone in the future does experience this.

Also, is there any concern that as the GoTrue store tokens become increasingly out of date with your custom store solution, there might be side effects? For example, I see that the GoTrue instance’s access_token expiration date is stuck in time as my user store refreshes tokens when necessary. Or perhaps you’ve found a way to go back and update the GoTrue instance after receiving refreshed tokens?

hi @vdadfar, the nf_jwt token has the httpOnly flag so it can’t be updated via client-side scripts which is probably why it wasn’t getting updated.

Some admin actions (like updating a user) can only be done in a secure environment such as a Lambda Function (as mentioned here). Just for reference, in case it wasn’t already mentioned.

I don’t have too much to add, but hopefully someone else might have some insight about your questions.

1 Like

Yeah, I discovered that the httpOnly flag was the issue. Rather than keep track of my own store for refresh tokens, I came up with an alternate solution that only requires a wrapper for the jwt function:

async jwtWrapper(forceRefresh) {
  if (localStorage.getItem('gotrue.user')) {
    let localStorageToken = JSON.parse(localStorage.getItem('gotrue.user')).token;
    if (localStorageToken && localStorageToken.expires_at > this.auth.currentUser().token.expires_at) this.auth.currentUser().token = localStorageToken;
  }
  return await this.auth.currentUser().jwt(forceRefresh);
}

great! thanks for sharing your approach :medal_sports:

Do you still use this code? I also get refresh errors but I can’t reproduce the synchronization problem between go true js instance and local storage. If I do the following, access_token and refresh_token are always in sync:

const debug = () => {
    if (GoTrueInstance.currentUser()) {
        console.log("---------");
        console.log("--START--");
        console.log("---JS---");
        console.log(GoTrueInstance.currentUser().token.refresh_token);
        console.log(GoTrueInstance.currentUser().token.access_token);
        console.log("---STORE---");
        const localStorageToken = JSON.parse(localStorage.getItem("gotrue.user")).token;
        console.log(localStorageToken.refresh_token);
        console.log(localStorageToken.access_token);
        console.log("---END---");
        console.log("---------");
        GoTrueInstance.currentUser().jwt(true);
    }

    setTimeout(debug, 10000);
};
debug();

any suggestions?

I’m trying to understand why that would be a problem. It’s a good thing that they’re in sync, correct?

Yes, they are in sync and it seems that it is working as expected.

In my production environment I also get refresh errors which I can’t reproduce so far. The Netlify gotrue server returns “invalid_grant” and “Invalid refresh token” which corresponds to this line in the code. That’s why I tried to understand the problem and solutions proposed in this thread.

Hey there, @simongarfunkel :wave:

I am glad everything is working as expected.

I just want to follow up and see if you are still looking for support or have a particular question in mind? If you are still experiencing refresh errors and would like further debugging advice we will need some steps to reproduce it. Have you been able to reproduce anything in the past nine days?

Reposting my comment from the Github issue:

After experimenting with this some more I am almost certain this is not a bug, but a common issue people create for themselves that can probably be avoided with some improved documentation.

For example, in the example here, which doesn’t use the remember me functionality, the user info is saved as cookie manually to prevent logging out on refresh. I think a lot of people, including myself, did this even with setCookies:true not understanding that auth.currentUser() would resolve under the hood. As a result I think people are inadvertently instantiating more than one GoTrue object.

In the initial example, it seems pretty clear that auth and this.netlifyIdentity are different instances of the GoTrue object and that is why the call to jwt in auth isn’t updating the state in this.netlifyIdentity . I think creating some documented examples of setCookies in combination with Vuex and Redux state management libraries is best solution here.

Didn’t have the time to investigate further. But will do in the following days. We definitely want to keep users logged in as long as possible and they shouldn’t get logged out already after a few hours. Will try to have something reproducible in a public repo.