6

Note: I fully acknowledge there may be something I'm missing in the picture, which is part of my reason for posting. I'd like to get the opinion of people more experienced than I am on authN/Z related implementations.

Here's where I am stuck on this whole "no refresh tokens on client apps":

The assumption I make is that without a refresh token, we also can't, for user experience reasons, always have a very short-lived access token, i.e. if we had an access token expiring every 15 minutes with no chance of refreshing we'd have to login again every 15 minutes.

My problem: by extending the lifetime of the access token we're giving a potential MIDM attacker a much more dangerous weapon, i.e. a token that gives access to the resources that could last days, or even forever (as I've seen in far too many projects).

To me, the simplest way to resolve such a situation is a refresh token, even one that sits on the front-end, because to my mind that still beats a long lasting ACCESS token.

This refresh token could be:

  • Also a JWT that expires, but maybe expires in a week or so
  • A "use only once" token that gets used to refresh the access token and then gets then replaced by a new refresh token

To my eyes, this approach vs. the long lived access token solves a few problems:

  • with this approach, I get to keep a short lived access token, so if it falls into bad hands it can't be used for long
  • if you think the refresh token is even worse, I disagree, because there's two things that make it better:

    1. once the main user refreshes his token again that previous refresh token won't be usable anymore
    2. if we assume someone steals our refresh token WHILE we're browsing, in a matter of minutes our browser would try to use that refresh token, fail (because someone else used it and so it's no longer usable) and prompt us to login, which will again completely invalidate the other series of refresh token obtained by the malicious entity
    3. even if 1 and 2 weren't true in all scenarios (i'm thinking multiple sessions/refresh tokens allowed so not a single stack or refresh tokens but multiple) i would still wager that you're better off invalidating refresh tokens through whatever procedure you want than you are invalidating access tokens. a JWT access token is valid in itself and the entire purpose of using one is that you can trust it by just verifying it, without having to do other expensive operations like checking with the auth server if it's blacklisted or not. That would essentially mean having to check this sort of thing for EVERY request EVER done with EVERY access token = not very scalable, especially if you compare this to just having to do this sort of thing ONCE every 15 minutes for the refresh token (you could keep a refresh tokens blacklist).

So, with all this said, is having a client side "use once" refresh token that also is a JWT and expires that unreasonable? We're avoiding having the access token last weeks, how can that be bad?

9
  • why? you can revoke access at any time on the server, for any reason. That's like changing your lock every day; would it make you any safer, or would the enormous hassle lead to a design compromise? If an attacker can somehow get the old token, why can't they get the new one(s)? What threat are you protecting against?
    – dandavis
    May 19, 2018 at 20:45
  • how though? how do I revoke access anytime? are you suggesting an access tokens blacklist? and should this blacklist check run on every resource request from any other service (e.g. in a microservices context) - my purpose with this approach would be to decouple these necessities (blacklisting/revoking) from the access token which is short lived and have them happen through the much less frequently used refresh token.
    – Salvatore
    May 19, 2018 at 20:59
  • it seems to me that throug this implicit flow + silent authentication approach I've seen discussed elsewhere (i.e. a self updating access token) we are effectively saying that if we can't have a steel door (which would be having a server app which can keep the refresh token safe) then we should not even bother with a wooden door, a plastic door, but we should keep no door at all because it's all the same.
    – Salvatore
    May 19, 2018 at 21:01
  • 6 of one, 1/2 dozen of the other. What's the diff between blacklisting and re-authenticating all the time?
    – dandavis
    May 19, 2018 at 21:11
  • 1
    @sarneeh frankly I just gave up discussing with people. Refresh tokens make sense to me, and when I discuss my point of view with people personally they tend to agree. I've certainly kept using them in my projects and will continue to do so.
    – Salvatore
    Mar 23, 2020 at 0:36

2 Answers 2

0

After some research and thinking, this is pretty much how I've implemented it.

I agree with your reasoning, and the access/refresh token is fairly well established best practice.

Your mechanics should have the ability to invalidate both the access and refresh tokens at any time in addition to a lifetime. This way, you can server-side invalidate whenever you suspicious something fishy is going on. Invalidating the access token forces the client to use the refresh token to get a new access token. Invalidating both forces the client to entirely authenticate again.

I also came to the same result with the "use once" refresh token: Whenever it is used, the used one is invalidated. Throw an error message if the client tries to refresh with an invalid token, this can be an indication of an attack. It also means you don't have many valid tokens hanging around if a client (due to a bug or something) refreshes more often than it needs to.

Invalidation can be done with a blacklist or whitelist, or through things like a counter in the JWT claims, similar to sequence numbers in TCP. Pick depending on your required security level.

0

There's no reason not to use a refresh token in the browser, and you're right that making them single-use is better. There are only two problems with your proposed design:

  1. You shouldn't use a JWT for the refresh token! The whole point of the refresh token is that it lets you work around one of the worst problems of JWTs, which is that they have to be very short-lived since there's no (practical) way to revoke them (e.g. when a user logs out); they only ever become invalid by expiring (unless you rotate the signing key, which invalidates all extant JWTs at once). Obviously this is as true for JWTs-as-refresh-tokens as for JWTs-as-access-tokens. Refresh tokens should be secure random values, hashed and stored in the database (hashed value as the primary key, with user/session data including stuff like access scope in other fields). One extra DB hit per user every five minutes or so is not going pose an unreasonable load on your server (or if it does, you have bigger problems).
  2. You propose invalidating all extant refresh tokens when a user logs in again. Naively this seems like it increases security, and indeed in some cases you might want it, but it's going to cause very bad user experience in a lot of cases. There are plenty of reasons to legitimately have a login session active at once on multiple devices, and you should only prohibit this on the most incredibly paranoid applications. You can mitigate this UX issue by tying the token to a device ID (that is randomly generated at first login and never changes); an attacker with XSS or browser storage access can steal that ID too and thus use the stolen refresh token, but you can detect when the same browser logs in again without breaking multi-device uses. The bigger problem, though - related to point #1 - is that you can't revoke the stolen refresh token if it's a JWT! (At least, not without giving up the statelessness that is the whole point of JWTs in most cases.) You say "fail (because someone else used it and so it's no longer usable)" but you don't know that unless you're storing server-side state of which tokens have been issued. If you're doing that anyhow... just use a secure random token, as mentioned above.
1
  • I didn't say anywhere I'd want to invalidate all pre-existing refresh tokens, but only the one being used to get a new access token. It goes like this: user tries to use access token > server says access token has expired > user uses refresh token to get new access token > user receives a new access/refresh token pair, and the previous refresh token is invalidated because it has been used It's been years since this question of mine, I can confirm that I've successfully used this pattern in all sorts of projects, small and very big
    – Salvatore
    Aug 15, 2023 at 10:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .