17

Currently, the access token expires in 15 minutes and the only info stored in the payload is the user id.

Each user has a balance which he can use inside the app. Should I query the database on each request so I can pull the balance? Would it be unsafe to store it the payload?

civ15
  • 301
  • 2
  • 7

3 Answers3

30

tl/dr: Although you could store the balance in your JWT and it would be safe from tampering, there are many ways in which the balance in the JWT might be technically valid but still incorrect. Therefore, you are probably best off checking the balance in the database everytime, and you certainly need to check it before making any transactions

There are two kinds of JWTs:

  1. JWS: Payload is in "plain text" and has a signature to confirm its contents
  2. JWE: The payload is completely encrypted.

These have slightly different use-cases. If all you need to do is verify that the data stored in the JWT is correct and has not been tampered with, then a JWS is fine (presuming you implement it properly and verify the signature on all requests). Therefore, you could store the balance in a JWS and later confirm that the reported balance is what you originally stored.

If you also want to keep the data private, then you can use a JWE. The encryption will also guarantee that the data is not modified (again, assuming you properly implement the JWE). Note that the only person who normally has access to the JWT is the end user, so we're talking about keeping it private from them - probably not necessary for your use case (h/t EdC).

Therefore, the balance is safe from tampering.

In either case you should check the balance with the database before making an actual transaction though because:

The balance might still be wrong. If another transaction was made without updating a JWT, or if an old JWT is presented (aka a replay attack), then you can have a valid JWT that has the incorrect balance. This is very likely even without active attackers. Consider a user who uses more than 1 app. In your hypothetical scenario, what happens if a balance is stored in a JWT in one device, and then the user logs into another device and makes a transaction? The data in the JWT for the first device is now incorrect, even though the JWT itself is valid and has not expired. This is just one of many ways in which valid but incorrect JWTs may happen.

There are many possible ways to fix issues with outdated data in a JWT, but in the end those fixes might be more work then simply checking the database every time. However, that's a balancing act that ultimately is up to you! There isn't one right answer there.

Conor Mancone
  • 29,899
  • 13
  • 91
  • 96
  • 1
    Great answer. Might want to expand on the question of who you're keeping the balance private from. Assuming HTTPS even with plaintext the info in the jwt is only visible to the same people that can see the page (same computer, same browser, disk cache etc) so JWE is primarily useful in this context only if it's info you have to hide from the user. – EdC Nov 26 '19 at 09:07
  • 1
    I have to wonder if using a token is the right approach here. But one possibility is that on the server side the credits are taken out of the "account" on generating the token. The token could then have a limited validity period and a reference in the database. If it is not used prior to expiring, the credits would be restored to the account. In effect you'd be converting credits from the server to another liquid asset for a certain amount of time. Of course implementing this correctly is another matter but at least it solves the problem of "reserving" credits and not allowing duplication. – G_H Nov 26 '19 at 12:08
  • @EdC good point, thanks - I've updated accordingly. – Conor Mancone Nov 26 '19 at 13:14
  • 1
    @G_H Indeed, I added a paragraph on the end in an attempt to touch on that topic. I think the short answer is that there are likely a lot of different strategies you could come up with to store the balance in the JWT and not run into trouble with stale data (malicious or otherwise). In this particular case I suspect that any potential solutions will be more work than just checking the database every time, but ultimately only the OP is in the position to make that call. – Conor Mancone Nov 26 '19 at 13:17
18

Yes you should query the database on each request. Using a JWT approach means you're relying on the client to send the correct JWT. If you're suggesting storing balance on the JWT then I presume that your idea is to generate a new JWT each time the balance changes. The problem is trusting the client to send the correct JWT.

Example

  • 00:00 - I access the app. A JWT is generated with my balance ($10) and a time to live of 15 mins. Let's call this "token 1".
  • 00:05 - I make a $5 purchase. This, presumably causes the server to generate a new JWT, with a balance of $5. Let's call this "token 2".
  • 00:10 - I attempt to make a purchase of $10. However I've altered my app to sent token 1 (rather than token 2). It's still effective and it has a balance of $10. Without checking the database, why wouldn't the transaction complete?
Andy N
  • 442
  • 3
  • 12
  • This can be solved quite easily; just make sure token 1 is invalid. It definitely does need solving, though - you can't trust the token just because it's signed; you also need a way to verify it's up to date. A simple way would be to work with transactions instead of balances, for example - a token has a last transaction id as well as balance, and when you want to create a new transaction, you need to supply a unique previous transaction id. You cannot create a transaction with an old token anymore. – Luaan Nov 26 '19 at 10:48
  • 2
    But you can really do the same thing even without transaction chains. In the end, _making_ the transaction requires you to touch the database anyway, and you can use that to ensure integrity. The main question is whether storing the balance in the token gives you any useful value, and that's a lot murkier. – Luaan Nov 26 '19 at 10:51
  • @Luaan you're 100% right. Even a solution which requires blacklisting/whitelisting JWTs requires a state to be maintained on the server. If the issue is just DB access then a caching solution would be preferable (with transactions that alter balance invalidating the cache). – Andy N Nov 26 '19 at 11:41
4

Querying the database on every request would negate the main benefit of using a JWT - that you don't have to hit the database. Assuming you are using JWT correctly, it is safe to store information inside it. Since it is signed, an attacker can not fiddle with the content.

Edit: While the signature makes sure that the payload is not fiddled with, it does not guarantee it is still up to date. See Andy N's very good answer.

Anders
  • 64,406
  • 24
  • 178
  • 215