Hi there DEV.to community!
Recently I've read some small posts on my social media that pointed out many security vulnerabilities of JWT. And the article below caught my eye.
The article above is a great one to tell you what might be the problem with using JWT as your main authentication method.
Whilst many things mentioned in the article above are valid, there is a workaround for them. I personally suggest you switch to other token types if you are just starting your project. For instance, I usually use Laravel Sanctum when my back-end is Laravel. But if you have a project that is already using JWT and there is no possibility to simply change the token type or you are simply enjoying JWT and willing to manoeuvre I encourage you to read this article.
I am taking an approach to counter the points declared in the article I linked above and might add some other points as well. In the end, you might suggest that what we have accomplished is not JWT anymore and you would be right in some way. Yet we should keep in mind that JWT doesn't enforce any structure to be put in the header or the payload and it only recommends them. You can refer to here for more information.
JWT size is big
As you know a JWT consists of 3 parts: header, payload and signature.
The header in a JWT usually consists of two properties. A typ
that is set to JWT
and alg
holds the algorithm type your signature is hashed with.
For instance:
{
"alg": "HS256",
"typ": "JWT"
}
If you are using multiple tokens for your system it is necessary to define these properties in your header so you may validate the token. But if your system isn't that big regarding the authentication types you might at least omit the typ
property to reduce a small size.
A body can consist of anything you want to make use of your token. What I usually put inside a payload is the id
of the user who the token is issued for.
The signature is a combination of your token's header and payload that is hashed.
Here are some symmetric hash algorithms you may use according to the JWT website:
Algorithm | Size |
---|---|
HS256 | 32 bytes |
HS384 | 48 bytes |
HS512 | 64 bytes |
If you want more security you may use asymmetric algorithms as well. Symmetric algorithms are still fine for most small use cases though.
So choosing a smaller size for the signature can reduce your token size overall. You might argue that HS256 is less secure than HS384 and HS512 respectively! While it is true, you should consider that each bit can be represented in two ways, a 0
or a 1
. Thus, a 256-bit (32-byte) signature is one in 115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,639,936
possibility. So if you are trying to use a 384-bit or a 512-bit signature without having a valid reason you are just wasting space.
With all these changes we can compare a predefined JWT to a custom JWT we've just made in size.
Note that we are using the same payload for both tokens:
{
"id": "123456"
}
Here is the token with the default header and using HS512 algorithm for signature:
Header:
{
"alg": "HS512",
"typ": "JWT"
}
Token:
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMzQ1NiJ9.Kvwu-9rwEgar8j6OH8HN57Vj5DQKYWDi-H9tfTWV3-TawrxIv_Wc_WLfTDNJeA3W9-omQhvi8xbZCacLEzMqQw
And now the custom-made token that omits the typ
in header and uses the HS256 to have the signature hashed.
Header:
{
"alg": "HS256"
}
Token:
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEyMzQ1NiJ9.mT15OKTKJMODj5xHKTmTTq9i8dqJ_b1-C1pY7RbDvRI
Comparing the size of both tokens we can see that the first token is 144 bytes long and the second one which we customized a bit is 85 bytes long. It is about 40% smaller.
Revocation of the tokens and security issues
So far we know that JWT is a stateless token that doesn't need a server to hold the information for use as the token already carries the necessary data. This brings up a problem! You cannot revoke a JWT that simply.
But it is not the end of the world and there are some techniques we may put in use to circumvent this issue.
Token blacklisting
Blacklisting a token is a simple way you might consider. To blacklist a token you can assign an id to each token in your payload and blacklist it in your database (or any kind of storage) so the token is considered invalid.
My personal way of doing so is assigning a UUID/ULID for each token inside the payload like this:
{
"user_id": "123456",
"token_id": "348087cf-8d57-4318-ac46-051c8ffd1ba5"
}
Some suggest to store the whole token and consider it invalid, but it raises two problems:
- If your token doesn't have a dynamic part like a issue date or an id to identify the token uniquely you might blacklist all the tokens for a user no matter when they are created, meanwhile blacklisting every token that will be created in the future.
- Storing a whole token in your database takes up a lot of space.
While this technique is good to implement it defeats one of the main purposes of a JWT which is being stateless. So this makes your tokens a hybrid regarding the stateful/stateless matter.
Token expiration time
Adding an expiration time to your payload can be beneficial in two ways. First point is that your tokens will be more secure if they are stolen there is a finite time the token can be used and the second is the revoking problem which can be solved with a short lifespan for the token. This approach also maintains the statelessness of the token.
Here is an example:
{
"user_id": "123456",
"exp": "1728146311"
}
There are some caveats for this method as well:
- If a token has a long expiration time it defeats the purpose of the expiration time.
- With shorter expiration time you need to implement a structure in which users can refresh their tokens to maintain their identity with your system, hence adding more complexity on your side.
Issue number of the token
This can be interpreted as the opposite of blacklisting a token. In this method, you can have an issue number for each token and store the latest token issue number along with the users details in database. For instance, when issuing the first token the token issue number for a user can include number 1. You may issue multiple tokens with this issue number and increase the number by one when you want to invalidate the previous token or tokens. As you might have guessed this can be problematic since you cannot simply revoke only one token and you need to revoke every token issued with that issue number.
There are more secure ways that are more complex than what you've read here to revoke a JWT that is beyond the scope of this article.
Security issues
A JWT's header and payload are base64 encoded, which makes them possible to see the raw content with simply decoding it. This can raise security issues of people knowing the secrets of a user if they access their token(s).
Such a case is avoidable in few ways:
- Never store sensitive data inside the payload and only store the data needed to verify who the claimer is.
- Encrypt the data stored inside the payload which you can decrypt yourself on the back-end.
- As always make sure you are using a secure connection such as HTTPS for web/mobile applications.
Feel free to let me know if I am missing anything or if I made a mistake. Hope this article was useful for you.
BTW! Check out my free Node.js Essentials E-book here:
Feel free to contact me if you have any questions or suggestions.
Top comments (0)