Storing info in JWT payload
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:
- JWS: Payload is in "plain text" and has a signature to confirm its contents
- 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.
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.
- 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?
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.