Security Tip: Your JWT Might Be a Forever Key!
[Tip #127] Without an `exp` claim, a JWT can remain valid forever, turning a leaked token into permanent access.
JSON Web Tokens (JWT) were designed as a tamper-resistant method of passing data between different systems, in the form of a set of claims. These claims are typically used as some part of an authentication and authorisation system - asserting who the bearer is and what they are allowed to do. Note that JWTs aren't encrypted, they're just signed to prevent modification.
JWTs look something like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30Example JWT
If you recognise the two eyJ in there, you'll quickly figure out that you're looking at a couple of Base64URL encoded JSON strings, separated by .:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
{"alg":"HS256","typ":"JWT"}
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0
{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}
KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
<HMAC Signature>JWT Decoded
We'll have to dig into how JWTs work in greater detail another time, but for now I just want to point out something specific that is missing here...
In the middle section, we have the user John Doe, identified by sub=1234567890 (their user ID), marked as admin=true, with this token issued at iat=1516239022 (18th January 2018).
I often see JWTs used to authenticate users, either via some form of API, in an SPA, or via magic links. But the problem is, this token is 8 years old (at time of writing), and as far as the application is concerned it's still perfectly valid! If the application accepts this JWT, then anyone who gains access to it will become Admin John Doe!
John might have left the company, their requests might have been logged somewhere and recovered in a data breach, their emails might have been compromised, etc. The possibilities are endless... like this token. It is effectively a forever key that will always allow the bearer in.
Oh, and JWTs cannot be revoked without some form of server-side state.
Without adding server-side state, rotating keys, or maintaining blocklists, the easiest way to limit abuse is to add an Expiration Time (exp) claim. This sets a limited time window on the JWT, so if it's stolen or discovered later, it's no longer useful.
For example, this payload gives the JWT one month validity:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"exp": 1518881422
}It would have stopped working on the 18th February 2018, and be completely useless to anyone who discovers it after this date.
Sure, you have to issue new JWTs periodically, but the alternative is a forever key you are unaware of and cannot block.
So make sure your JWTs have a reasonable exp set!
Oh, and make sure your JWT package actually enforces exp, apparently not all of them do by default. 🤦
If you found this security tip useful? 👍
Subscribe now to get weekly Security Tips straight to your inbox, filled with practical, actionable advice to help you build safer apps.
Want to learn more? 🤓
Upgrade to a Premium Subscription for exclusive monthly In Depth articles, or support my work with a one-off tip! Your support directly funds my security work in the Laravel community. 🥰
Need a second set of eyes on your code?
Book in a Laravel Security Audit and Penetration Test today! I also offer budget-friendly Security Reviews too.
Finally, connect with me on Bluesky, or other socials, and check out Practical Laravel Security, my interactive course designed to boost your Laravel security skills.1