Caveats to keep in mind while working with Json Web Tokens
JSON Web Tokens (abbreviated as JWTs) are digitally signed tokens that can be used to verify the authenticity of an incoming request, as well as ensure the request is authorized to make the request being made. Jwt.io has a wonderful article explaining what a JWT is, its components, and how it is used. If you are new to JSON Web Tokens, I highly recommend giving this blog post a read before reading on, because this post assumes you are familiar with a JWT and know how it works.
In this post, I would like to talk about a few things I learned from implementing and using JSON Web Tokens. Before we jump into things that need to be taken into consideration while implementing JWTs, it is important to note that one of the most important characteristics of a JWT is its immutability. While immutability is good, it can also be inconvenient because old JWTs cannot be invalidated without additional work. So with that in mind below are a few JWT-specific considerations for your next project.
No sensitive information on a JWT
Anyone with a JWT token, even if they do not have access to the key(s) used to sign it, could potentially use jwt.io to view the contents of the token. This is because the key(s) used to sign the key only help with verifying the issuer (to make sure the token wasn’t tampered with since it was generated and issued) and do not mean that the JWT is encrypted by default. Digital signatures and encryption are not the same and serve different purposes; digital signatures are used to ensure the authenticity of the issuer while encryption is used to pass messages between entities in a secret manner.
This does not apply if you have a layer of encryption on top of digitally signing a key but if that is not the case, assume that JWTs issued by your application are available to the whole world and anyone can see the contents in plain text. With this constraint, it is important to make sure that JWTs do not contain any sensitive information. If there is any information that — in the hands of a malicious user — could cause harm, avoid putting that in your token.
Reasonable expiry times
JWTs are a great alternative to having users enter and re-enter their username and password credentials constantly for every API call that requires some form of authentication/authorization. Therefore, having an optimally lived JWT is important because JWTs could be stolen and that means anyone with access to a valid JWT could impersonate a user and steal their information. To avoid this, it is important to make sure that JWTs are not valid for long periods of time; especially if your application processes sensitive data.
JWTs that can live for a week opens up a much longer window for attack. It might be a good idea to restrict them to a day or less. This needs to be balanced with how often we want users to enter their user name and password to refresh tokens. If we don’t want users to present their username and password in exchange for a JWT often, then we could also allow users to refresh JWTs using non-expired and valid JWTs. This implementation (when combined with a countdown on the UI to keep track of when a JWT expires and using it to fetch a new JWT just before the one in hand expires) could be a good solution!
Regenerate parts of JWT when user credentials changes
To get a JWT for the first time, users are expected to present their username and password. So let’s say that halfway through the lifetime of a token, the user decides to change their password. When this happens, it is good practice to make sure that all previously issued JWTs are no longer valid. But if we have already issued a JWT and there is no way to change a previously issued JWT, then how might we do this? Great question! While there may be several solutions on the internet (including keeping track of a list of invalidated JWTs per user until they expire etc), my favorite so far is to add a new field to the JWT that is backed by storage (say database) and regenerating the value of that field every time a credential change occurs!
This new field will need to be three things:
- Controlled by the backend
- Generated randomly (so not predictable)
- Will need to be exposed on the JWT. This way we have an additional flag that can be used to track changes to user information that in turn can invalidate previously issued JWTs for a given user!
For example, if we call to add a new field to our JWT and say “User A” is issued a JWT token with a value of “laek-sjrt856w-5ikad-fmsv” for that field when the user tries to get a JWT for the first time. Next “User A” decides to change their password with this JWT. Now we regenerate the JWT field to be “8475w-rjhgdsl-kfut9w-5830” and store it. This way when previously issued JWTs with the old value is used, the entity using them will receive a 401 meaning if “User A” needs a new token then they re-login, and this way we have ensured that old unexpired tokens cannot be used even if stolen.
Pick one: username and password or JWTs for APIs
Depending on the sensitivity of the data processed by your application, what I am going to say is a matter of personal preference. More often than not, if your application exposes multiple APIs and you plan on using JWTs to authenticate/authorize requests to those APIs, it might be a good idea to consider whether you want to lock down APIs that need JWTs to be accessible only using JWTs; meaning that even if a valid username and password credentials are presented, the application would reject the request if the request does not consist of a valid JWT. This is a good idea because it helps ensure that there is only one mode of authentication/authorization for APIs which in turn reduces attack options and also ensures that applications (e.g.: UI applications) clearly know that backend APIs can only be accessed using JWTs and username/password credentials.
Small claim key names
JWTs can quickly grow in size and get very large. Most of the pre-defined claims have three-letter keys (sub for subject, exp for expiry, etc). So for user-defined claims, it might be a good idea to restrict the name of the claim key to be three characters long (jwt.io recommends three character long claim names as well). Depending on the library/framework your application depends on, nothing may break if there is a custom claim with a more-than-three-letters claim key. But this is a suggestion to keep the size of a JWT in check.
JWTs are a wonderful way to authenticate and authorize requests without forcing end-users to provide their username and password. While there are some very good libraries and support from popular frameworks to generate and verify JWTs, there are certain guidelines that applications may need to be added on top of those rules to ensure that an application’s security is not compromised. These don’t completely mitigate ALL security risks but get us a step closer to making informed choices. Thanks for reading, I hope you found this helpful!