Logged in on client, not server

I’ve got a Vue.js application making use of Netlify identity and functions. I’ve noticed something odd. I can reload my Vue app after some time, let’s say 20 minutes, and I’m still considered logged in (checking via currentUser() on an instance of GoTrue), but a call to a serverless function (with passing the access_token) will return an error since the backend doesn’t respect the token anymore.

Logging out and logging in always fixes is - but there seems to be a timing issue I’m messing up. Any ideas?

So I’m seeing that the token has a 60 minute timeout. Would currentUser() return a valid value even if the token expired?

Hi @cfjedimaster, I don’t think that the currentUser() is automatically/dynamically updated. It will update the next time the token is refreshed as described here: Netlify-identity-widget currentUser object not refreshing after lambda update - #2. Let me know if that helps.

Hmm, so it kinda sounds like I could .jwt() if I detect the user just hit my site and was previously logged in. Does that sound right?

The .jwt() function has built-in checks for whether it has to actually do anything your best bet is to always put that in your chain!

Blah! How’d I miss this question. Greetings again @cfjedimaster :wave:t2:

Hm. .jwt() certainly isn’t designed to be a mechanism for determining whether the current user was previously logged in… but I think we should take a few steps back here. Warning: classic long-form @jonsully post ahead.


Stepping back to conceptuals, I want to split the premise of “having a JWT” and “Netlify recognizing the user”. I know that sounds vague, but let me expound on the former. If a user submits a valid username/email and password, they will get a JWT. Because JWT is a stateless authentication system, simply having the JWT proves that you are who you say you are. The exp field in a JWT is actually optional per the JWT RFC (sorry, there’s just no way to send a link to a specific header of an IETF RFC… use Cmd+F and search for the "exp). So to the point of your app — the fact that .currentUser() returns a JWT is itself proving that your user is who they say they are: they are in possession of a stateless authenticating credential that was signed correctly by the issuing entity (GoTrue). If you just needed to use the .currentUser() data to hydrate a header in your nav-bar that says "Hello ${user.firstName}", that would be totally valid. It doesn’t matter whether the JWT has an exp or not; the first name contained in the JWT was the valid and set first name when the user proved their identity.

Cool. But Netlify does implement the exp field. We have to understand this as a Netlify-imposed logical addition to a stateless auth system. Just because Netlify sets an exp value doesn’t make the local JWT invalid even once the exp time passes. The exp value really only matters when you send that JWT back to Netlify, which is exactly what you’re doing when you send it along for an Auth’d Function. Netlify receives your Authorization header, checks the signature of the JWT against the signing key GoTrue uses, and checks if the exp has passed. Again, technically the JWT alone proves that you are who you say you are, but for safety reasons, Netlify has imposed this additional layer of ‘expirations’ . For that reason, and as @snorkypie noted via this GitHub discussion, it’s advised to always run .jwt() before calling an Auth’d Function. Just to make sure the JWT is un-expired before sending it back to Netlify.

Anywho, all that to say, I don’ think there’s anything wrong here. The software is doing what it should, it just may not be the most intuitive with a stateless auth system. .currentUser() indeed gives you back the current user, which is valid regardless of any exp. It just isn’t necessarily valid when passing back to Netlify.

…whiiiiiiiich (shameless plug) is something I did my best to fully fix in my react-netlify-identity-gotrue library :stuck_out_tongue_winking_eye: it automatically keeps everything fresh so it all ‘just works’ :smiley: but that won’t help you in this case @cfjedimaster since you’re on Vue :sweat_smile:

Anywho, hope that helps and/or learns!


Jon

Thank you for this. One clarification - you said to run .jwt() before currentUser()? Or did I misread?

No problem :slight_smile:

Mmmm. I guess my conclusion is that they’re fundamentally different methods made to accomplish different things. .jwt() is made to give you back the raw JWT string ‘behind’ the current user (and refresh it before doing so if it’s expired) — in a sense, this method is what syncs you up with Netlify’s can’t-be-expired logical layer.

Conversely, .currentUser() exclusively just gives you the current user javascript object that gotrue-js is holding. It doesn’t know or do anything about refreshing or exp, and that sort of makes sense when you consider that the user object has nothing to do with the JWT. Yes the JWT contains the user object, but once you pull it out, the JWT is just the shell that it came from. You’re asking for what’s in the shell when you call .currentUser(). So it gives you what (was) in the shell. It doesn’t worry about whether or not the shell expired according to Netlify.

Anyway, to your point, I think that running .jwt() will also update the user object, so if you have reason to believe that your user was updated by some external factor (e.g. you update a user Role from the Netlify back end), running .jwt(true) followed by .currentUser() should give you the latest data for that user client-side.

The “what’s in the shell” vs. “the shell itself being expired according to Netlify” is probably a good metaphor. Could go further there :rofl:

Hope that helps!


Jon

That kinda makes sense. I really do wish there were more tutorials out there about this. I’m going to try to make a simple example of this (site requires login, site uses a netlify serverless func that requires auth), and run it by you if you don’t mind for a sanity check?

Sure. Happy to look it over. This does exist, but it is pretty-much made for the library I referenced earlier :stuck_out_tongue:

source here


Jon

I don’t use Gatsby. :wink: I need to build this myself w/o a framework, or a minimal one, to get a feel for it.

1 Like

Fair 'nuff! Just let me know when you’ve got something whipped up and I’ll check it out :+1:t2:

@cfjedimaster
So this may help to clarify (having gone done this road myself).

The TL;DR of it is this:

currentUser() isn’t making an external api call.
jwt(), also, isn’t making an external api call unless the current token is expired or you have asked it to force refresh: jwt(true)

Forcing refresh of the token before function calls should do it, because it will error and call clearSession() which calls removeSavedSession() to delete the localStorage values, and nulls the currentUser.

As an additional FYI, decoding and using the access_token values (see the very end of all this) in app can help you keep external changes in sync.


The long path to the above:

login() and currentUser()
The user created upon logging in is saved in localStorage.

You can follow the path from login() to createUser() to the User class’ _saveSession()

 _saveSession() {
    isBrowser() && localStorage.setItem(storageKey, JSON.stringify(this._details));
    return this;
  }

Now logged in, a call to currentUser()

  1. first calls the User class’ recoverSession(), which in turn is checking the currentUser defined outside the class, but assigned this inside the User class constructor() Here, currentUser() is getting and returning the user object created at login().
  2. If no currentUser is found, it then checks localStorage:
    const json = isBrowser() && localStorage.getItem(storageKey);
        if (json) {
          try {
            const data = JSON.parse(json);
            const { url, token, audience } = data;
  1. if the localStorage data is bad (missing url or token), then it uses the passed-in apiInstance or creates a new api instance and passes that to a new User to recreate it.

But no call has been made to fetch any data since login()


.jwt() and refresh

the .jwt() method of the User class:

  1. first gets this.token by calling this.tokenDetails() (it just returns this.token). this.token is defined in _processTokenResponse() which is called in the User class contructor() (and updated elsewhere). So, it’s just getting the current token.
  2. it checks to make sure the token exists, then destructures to check for expiration. It also checks here if you asked to force a refresh (by passing true as a parameter to .jwt())
  3. if a refresh wasn’t requested and token is not expired, it returns the current access_token. Which means, the one available in this.token in the User class instance.

Again, at this point no new data has been fetched. If the token is not expired, you are getting the current token, not a new one.

Forcing the refresh is simple, just pass true like this: .jwt(true) and then the method will call _refreshToken()

The _refreshToken() method:

  1. checks for any waiting refreshes, then:
    return (refreshPromises[refresh_token] = this.api
      .request('/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: `grant_type=refresh_token&refresh_token=${refresh_token}`,
      })

Once the new data is returned, it calls _processTokenResponse() to save the token to this.token, it calls _refreshSavedSession() that calls _saveSession()to store the value in localStorage, and then returns the access_token.

Note though, that _saveSession actually saves the response from _details() into localStorage. That is, via an iteration of the properties on the instance’s this. Meaning, the only part of the new token from jwt(true) that gets stored to localStorage is the access_token assigned to this.token (via _processTokenResponse()).

The rest of the data saved (and then later returned by any call to currentUser() will be the data that passed through the constructor upon logging in. Missing that point caused me many headaches.


Refreshed Access_Token and User Data
The access_token returned by a call to .jwt(true) is obviously not full user data, but it does contain the following, up to date from the sever, properties:
app_metadata, email, exp, sub, user_metadata

Since important things like roles[] are in app_metadata, the access_token returned by .jwt(true) can be decoded and used to keep values up to date/in sync with the app.

jwt-decode can be used to decode the token like so:

import jwtDecode from 'jwt-decode'
const user = auth.currentUser()
const accessToken = await user.jwt(true)
const freshData = jwtDecode(accessToken)

const userRole = freshData.app_metadata.roles[0]

I hope that helps, I know wrapping my brain around what was
happening took a little while :grinning:

2 Likes

Hey folks, finally getting time to look into this. I greatly appreciate your help.

I was wrong about calling jwt() before as it’s a method of the user object. This is what I’m thinking but it feels wrong. Again my intent here is just to support, “Is this user logged in and can call Netlify user-protected functions”

isLoggedIn() {
	let user = auth.currentUser();
	if(!user) return false;
	//force refresh if need be
	user.jwt();
	return !!auth.currentUser();
},

Is this valid?

I know this is going to sound pedantic but I promise I don’t mean it to. What do you mean by isLoggedIn? Because if auth.currentUser() returns an object, the user is logged in. That user may not be able to issue a request to a Function validly, but that doesn’t mean they’re not logged in. If isLoggedIn is just a preface to calling a Function, I totally understand, I just want to get the idea straight here.


Jon

If you’re trying to be sure the user is still logged in on server before calling on a function, etc, then force the refresh .jwt(true).
Anything else is only checking local value and expiry of that local value.

isLoggedIn() {
    let user = auth.currentUser() // get user from localStorage
    let access_token = await user.jwt(true) // if this fails, it will clear localStorage session
    return !!access_token // if you have a new access_token here, user is logged in on server
}

edit:
and yeah return !!auth.currentUser() may be the better to be sure.
But point is to force refresh.