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...
For further actions, you may consider blocking this person and/or reporting abuse
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.
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:
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.
Exactly this
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):
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 ?
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)
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.
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 thewindow
object as well.The
localStorage
andwindow
are globals. Is presumably easier to extract info from globals then from the encapsulated application logic code.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)
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.
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:
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 :)
also this:
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.
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.
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.
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.
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.
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
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.
I've been looking for something like this for quite some time now.
Keep up the good work!
Thanks a lot ! Glad you like it !