DEV Community

Cover image for The Ultimate Guide to JWT client side auth (Stop using local storage!!!)

The Ultimate Guide to JWT client side auth (Stop using local storage!!!)

Kati Frantz on November 23, 2020

Hello, my name is Kati Frantz, and thank you so much for checking out this tutorial. I want to talk about how to handle JWTs effectively and secure...
Collapse
 
daniguardiola profile image
Dani Guardiola_ • Edited

With all due respect, this is misinformed. Whether the token is in local storage, cookies, or in JavaScript memory, and whether you renew it frequently or not doesn't really change the actual issue.

The issue is that your application or website is vulnerable to an XSS attack.

Obscuring the location of your token (which you actually fail to do as you have a renovation token that can be used to generate tokens) does not solve the issue.

Shortening the lifespan of tokens does not solve the issue, it just gives attackers less time to access your account, which can be easily circumvented because they still have access to the renovation token.

The one and only issue here is XSS vulnerability. The rest are just security recommendations that could help, but a dedicated hacker still has a way in.

I'm worried people who read this post will think obscurity is the solution to this scenario. The only solution is fixing all XSS vectors in the app.

Collapse
 
mattother profile image
mattother

I don't really see that you are gaining anything by keeping the token in memory. The refresh token is already persisted. And if anything it seems like you are making things worse, but maybe I'm missing something.

So for example:

  1. Login to the site
  2. Go to some page
  3. Open a new empty tab
  4. Paste in a url to another page on the site I want to go to (ex. /super-goats)

In this circumstance I would expect to have a valid session in both tabs, but I don't see how I could have.

If you are invoking the refresh token in the second tab, I would expect the token existing in memory in tab 1 to now be invalidated, in which case the page no longer works correctly. In the best case it will refresh the token again, but at this point you are basically reauthenticating a bunch of times if the user is jumping between tabs.

Again unless I'm missing something.

Also, to me this doesn't solve anything. You really had 2 issues and I only see one being solved here.

1. Tokens lasting forever or for a long time.

This should never be the case and tokens should always be provided with refresh token such that tokens are constantly refreshed.

But there isn't a good reason here for keeping the auth token in memory, I don't see that you gain anything keeping it in memory.

2. You site allowed an XSS attack

Really this is the point that needs to be addressed. I think your efforts would better be spent by investing in stronger Content Security Policy that better prevents XSS on the site.

Really if this isn't solved you haven't gained anything. I can still access the refresh token and hijack the session and I still have access to the token in memory.

So overall I guess, I don't see that you've solved anything by moving it to memory. You've just made the user experience worse.

Collapse
 
daniguardiola profile image
Dani Guardiola_

Exactly this

Collapse
 
rad_val_ profile image
Valentin Radu • Edited

The problem hits when you try doing this cross-domain (e.g. with the SPA statically served from myapp.com and the API from backend.com):

  • setting the refresh http-only cookie is not trivial anymore
  • even if you manage to set it, the browser won't attach it to the cross-domain refresh request
Collapse
 
bahdcoder profile image
Kati Frantz

Hello, thanks for sharing. In my experience, even with the sample project I shared, the browser always attaches it when you correctly configure the HTTP client and the backend server.

Can you please share a scenario where this won't be the case ?

Collapse
 
rad_val_ profile image
Valentin Radu

If you try to run your example in a setup where the domains are actually different (for cookie purposes, browsers don't count different ports as different domains see this RFC: stackoverflow.com/questions/161217...), it will fail. You have to enable 3rd party cookies to make it work, which comes disabled by default (in Safari at least and probably in all modern browsers)

Thread Thread
 
bahdcoder profile image
Kati Frantz

Thanks for sharing this. The only scenario where it works seamlessly is in a situation where both sites run on the same domain (can be different subdomains, but must be the same domains).

This actually makes me very curious. How do third-party authentication providers persist sessions ? Take Auth0 for example, how do they persist sessions for the application they're authenticating, given its on a different domain?

Looking at the source code now for auth0-spa-js, and if you have a chance, please have a look. You might see what I can't see.

Thread Thread
 
rad_val_ profile image
Valentin Radu

You're right, if you set the cookie with the right domain pattern (i.e. .example.com) it should work between subdomains, but would still fail between domains.

One of the way you can use Auth0 is similar to what you've presented here, more or less, at least when it comes to third party cookies. Then again they don't recommend it (auth0.com/docs/login/embedded-logi...) and it doesn't work on many modern browsers (you can mitigate this by using a custom domain)

Also, it makes little sense to keep the bearer token in memory: whomever has access to the localStorage through XSS can scan the window object as well.

Thread Thread
 
ptejada profile image
Pablo Tejada

The localStorage and window are globals. Is presumably easier to extract info from globals then from the encapsulated application logic code.

Collapse
 
cyberhck profile image
Nishchal Gautam

Why does your refresh token look like a jwt?

Having api calls on separate domain doesn't work in case user blocks 3rd party cookies.

Your spa shouldn't do a set timeout, on every request, if the jwt has expired, it should refresh and set new jwt, this belongs in client side middleware.

Setting in cookie is good only if you know your api is being served under exactly same domain.

We do need to save jwt in cookie on client so that we can do server side rendering (else you don't have access to jwt in server side)

Collapse
 
bahdcoder profile image
Kati Frantz • Edited

Hello, thanks for sharing.

First, the refresh token is a JWT, but it cannot be used to gain access to the API. It can only be used to get a new access token.

Secondly, you're right about 3rd party cookies. The ideal situation in this case would be when both sides of the application are running on the same domain.

Thirdly, there's really no disadvantage in my opinion to use a set time out at this level, its just some tiny background job that makes sure the backend never even needs to return a 401. Another approach like you mentioned is handling this in client side HTTP middleware. Some people prefer it this way, but I prefer not having to do a 401 check, requesting an access token, and making the request again when the user is waiting for an API call to resolve.

Also, This sample project works neatly across different domains, and I make a mention about that at the end of my article. Cookies can be exchanged cross domain, you just need to configure them.

And, for SSR, using something like Next.js for example, you can always either forward the cookie to the browser, or still handle it on Next.js own server without too much problems. Maybe I'll make a tutorial on this in future.

Thanks for sharing your ideas, I hope this makes sense to you.

Collapse
 
cyberhck profile image
Nishchal Gautam

A few things, I don't agree with:

  • When you say refresh token is a JWT but can't access, are you storing that in db? What is the expiry time? Because it must be enough long lived, say you did 3 months, and you're relying on JWT properties (signing key) to validate refresh token, then when I get access to your refresh token, how will you log me out? Unless you keep a list of blocklist of refresh token, (then how would you block those token anyway? Because you don't know, as you didn't save them), this means once I get a refresh token, you are duped. I suggest falling back to opaque tokens which reside in db, and you can show all the sessions in user's security page and you can simply revoke those refresh token, which means no more new JWT at least. (and it's more cheap to keep a blocklist of JWT with lower expiry time if you need instant signout)

  • I'm not saying middleware does a 401 check, if you write your middleware correctly it shouldn't return 401 because of expired JWT, here's a kinda rough middleware for tinka:

export class RefreshTokenMiddleware implements IMiddleware<IFetchRequest, Promise<IFetchResponse<any>>> {
    public async process(options: IFetchRequest, next: (nextOptions: IFetchRequest) => Promise<IFetchResponse<any>>): Promise<IFetchResponse<any>> {
        if (this.hasJwtExpired(options)) {
             const jwt = await refreshJwt(...)
             // save this jwt so we can use later
             options.headers["authorization"] = jwt;
        }
        return await next(options);
    }
    private hasJwtExpired(options: IFetchRequest): boolean {
        // if jwt doesn't even exist, I guess you can think of it like a guest mode, and you should just return false,
        // compare the expiry timestamp with current timestamp and return true or false,
    }
}
Enter fullscreen mode Exit fullscreen mode

This way just before you make an API call, it ensures there's a valid JWT on request. (There's nothing wrong with having setTimeout but think about opening a tab in new browser, how many JWT do you want to actually store in db, a lot of people use multiple tabs and that'll invoke refresh token in each tab if we do setTimeout, but with this since it's done just before making a request, opening a new tab is okay.

"You just need to configure them." I don't think you understand what we meant. A lot of people are now blocking 3rd party cookies, I block 3rd party myself, which means, no matter what you do on your end, since I'm blocking 3rd party cookies, it simply won't work.

No, I mean I was agreeing with keeping tokens in cookie, but JWT must be accessible on client side, because if not, then how can I add authorization header for my API calls? If you want to say "don't use authorization header, just let API read those cookies", then I'm gonna stop even trying :)

Thread Thread
 
cyberhck profile image
Nishchal Gautam

also this:

Google will join Safari and Firefox in blocking third-party cookies in its Chrome web browser. However, unlike those browsers (which have already started blocking them by default),

Thread Thread
 
daniguardiola profile image
Dani Guardiola_

This post is misinformed. The only thing that had to change about the situation is to stop being vulnerable to XSS. As long as there's a token, it can be stolen, no matter how many steps it takes from having the token to using it to access the account. Making those tokens short-lived is a good practice but doesn't change the facts.

I'm with you brother.

Collapse
 
jwhenry3 profile image
Justin Henry

Let's face it, web traffic is compromised. There is no way around it. The most you can do is server-side restrictions, validation, and time-sensitive sessions. If you truly want to avoid some concerns, move away from REST and towards Websocket connections with one-off token grants that must be renewed before each connection. At the end of the day you need to perform cost-benefit analysis and gauge what solution fits best for the needs of the company and your application.

What can you accomplish one way that you cannot with the other? How convoluted is one solution when it could be much simpler? Is the security gain really that much more, so much so that it makes all other solutions obsolete?

When deciding on the right authentication protocol for your application, perhaps what you need is OAuth2 from the client to a 3rd party domain and do not persist the tokens at all, rather rely on the 3rd party domain to store that securely on their end (even though it may be another one of your apps). Putting up barriers without compromising the integrity of your application may be all that is needed.

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

I wonder - why won't you persist this JWT on a http-only cookie? given that you have the means to include this cookie for all subdomains and if another domain requires it, go ahead and set a cookie for it as well, after all it is your site's users sessions.

Collapse
 
dbroadhurst profile image
David Broadhurst

If you want to be safe then don't make your own auth. Cognito, Auth0, Keycloak, Firebase Auth all provide secure-auth that's easy to implement and practically free.

Collapse
 
andreidascalu profile image
Andrei Dascalu • Edited

Dumb question: why do you need to tell the spa when the jwt expires? Presumably the spa has access to the public key which will allow access to the expiration on the token.

Collapse
 
cyberhck profile image
Nishchal Gautam

You don't, you don't even need pub key to know when it expires, client side doesn't need public key at all,

Use a middleware to do refresh token when it's expired, that way if the app is idle, it won't do a refresh

Collapse
 
bahdcoder profile image
Kati Frantz

No dumb questions here Andrei, thanks for asking. This is just for convenience. You can tell the client when it expires, which means the client knows when to refresh, and does not wait for an unexpected expiry before refreshing. Like @nishchal mentioned, you can always wait for the backend to return a 401, and then either automatically logout the user and redirect to the sign in page, or refresh the token and keep the user's session.

Collapse
 
atulgairola profile image
atul-gairola

I've been looking for something like this for quite some time now.
Keep up the good work!

Collapse
 
bahdcoder profile image
Kati Frantz

Thanks a lot ! Glad you like it !