1

So I wrote the following logic for my web app:

When a user interacts with the website it initiates a Backend call. In the backend every endpoint has multiple middlewares, of which there is a JWT verification step, if it succeeds it goes to extending the token by deleting the iat, exp, nbf, jti values in the decoded JSON and then sets a new token with a new expiration and new issue date etc.

  • Does this logic have any security flaws? As you can see from above there is no refresh token used, but it seems to have enough functionality.

  • Why are refresh tokens even used if this is enough?

5
  • Hi, I think the reason is that a lot of apps use crypto verification for JWT tokens which doesn't support logout. However, they use database verification for refresh tokens, which does. There may be other reasons but I think this is the main one.
    – paj28
    Oct 14, 2022 at 9:28
  • @paj28 logging out just equals deleting the cookie via the backend for my app. So I still don't get it.
    – Munchkin
    Oct 14, 2022 at 9:48
  • That's not proper logout. Consider the case where a user accidentally leaves a login active on their work computer. They're at home now and they want to logout of everywhere.
    – paj28
    Oct 14, 2022 at 14:39
  • @paj28 is "proper logout" actually possible with JWT?
    – user253751
    Oct 14, 2022 at 15:17
  • @user253751 - Yes it is possible. One approach is to have a server-side set of blocked tokens. More common though that access token is a JWT, and in a "logout all devices" scenario those remain valid for 15 mins, but the refresh token is invalidated server-side.
    – paj28
    Oct 14, 2022 at 16:14

1 Answer 1

2

There are several security dangers inherent to doing this. There is also a problem in that it encourages you to make bad security decisions for the sake of user experience.

One way it would matter to security directly is if the JWT is at higher risk of leaking (e.g. due to being used in more places) than the counterfactual refresh token (used only in a single request to a single service), in which case there's a risk of the JWT being stolen by an attacker, and its lifetime extended indefinitely, for permanent access that wouldn't have been possible if you'd required a separate token for refresh. On the other hand, you shouldn't be waving your JWTs at anything untrustworthy, no matter how short their lifetime, and also in many cases the refresh token is just as exposed (or not) as the JWT.

The other way it matters to security is if you want to enforce a maximum session lifetime, beyond which refreshing isn't possible. Normally, that's something you'd store in your DB alongside the refresh token, specifying the latest date at which the token is accepted, and if you rotate the refresh token after use you simply update the token - but not the final expiry - in the DB. Without a refresh token, you'd want to add a field to your JWT that specified the maximum session age, and when you re-issue the JWT, you update the short-lived JWT lifetime but do not update the max session age. Of course, when validating the JWT, you would only look at its short-term lifespan (so a previously-leaked token can't be used again), and when refreshing the JWT, you'd make sure that the expiry timestamp is no later than the overall session expiry value (which means eventually the user can't refresh anymore and must log in again, as intended). This isn't a common feature with JWTs, but you could add it; it's very much the sort of information one can store in the body of a JWT, and re-sign on refresh. So that's not really a problem, more just a bit of extra logic you'd need to add.

Last but very much not least, there's no way (inherent to JWTs) to revoke a session if you suspect it's compromised. This is a fundamental weakness of JWTs in general; if you want to preserve their statelessness, you can't make the process of validating them require checking any server-side storage (such as "a server-side set of blocked tokens"), so there's no way to force server-side logout (from the original session or another one). As with the last item, this can be solved in one way, though: rather than trying to refresh the JWT on every backend call, you only do it if the JWT is nearly expired - say, less than a minute left - as that way you only occasionally have to take the performance hit of the DB lookup. You may also have to store a list of issued JWTs in the DB - otherwise, how does the server know which JWT it shouldn't trust anymore when you try to end other sessions? - so at this point the benefit of your JWT-only approach over adding a refresh token or even just using opaque random tokens in place of both JWT and refresh token is looking pretty thin, but it's an option.

The other problem is what this means for user experience. What you're essentially proposing is a rolling session, where it lasts forever (or at least up to some max lifetime) if and only if the user is never idle for long. The idle timeout in this scenario is the actual max lifetime of the JWT, or - if you do the "attempt refresh, including consulting a server-side table, only on requests shortly before expiry" thing - the window of time in which the JWT will, if validated, get refreshed. From a user experience perspective, you want this window to be as long as you can; it sucks to get up from your computer to use the bathroom and come back to find your session dead. From a security perspective though, you want that window to be as short as possible, because you can't trust the user to have locked their computer while they go to the bathroom and maybe somebody came by and stole the token(s). Balancing that is tricky.

5
  • How can the lifetime of a JWT get extended indefinitely if the JWT has a proper HMAC(256 I think) signature and there's an expiration check on the server side using the HMAC(256?) secret? And if I want to implement OIDC into my app, JWT functionality is no longer necessary I would assume, right?
    – Munchkin
    Oct 17, 2022 at 7:03
  • I'm not sure what you mean, "expiration check using the HMAC secret", that's not what HMACs are or how they work. The indefinite extension wasn't for any given JWT, though, it's for the constant refreshing; if you don't store state on the server (as expected with JWTs) and also allow a JWT to self-refresh (not normal, for the following reason), then a user - or an attacker - can keep their session alive indefinitely by refreshing the JWT any time it gets close to expiry. Preventing that requires either server-side state or a max-session-age field within the JWT, separate from the per-JWT exp.
    – CBHacking
    Oct 17, 2022 at 9:43
  • Aaah ok, but doesn't the removal of refresh tokens solve this problem per se? Then there is only one token, which will definitely expire soon anyways.
    – Munchkin
    Oct 17, 2022 at 9:45
  • Unclear what exactly you want to do with OIDC, and that is maybe a separate question. OIDC does require at least briefly using an OAuth access token - which is almost certainly itself a JWT - but you can in theory avoid issuing your own session token if you want to just use the OAuth or OIDC response token (which is almost certainly a JWT, but asymmetrically signed so you can verify it yourself). Re: removal of refresh tokens, the JWT will expire soon if unused but your question is premised on making the JWT able to act as its own refresh token, so an attacker would use it to get a new one.
    – CBHacking
    Oct 17, 2022 at 9:49
  • I might need OIDC to have roles support in my app etc., to integrate with other services via OAuth2.0. Is it really a refresh token though, I mean it uses the same secret each time, but as I understand it generates a new token without depending on the other/old token. I might be completely misunderstanding this, I'm sorry in that case.
    – Munchkin
    Oct 17, 2022 at 10:20

You must log in to answer this question.

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