This article assumes that you have a working knowledge of how JSON Web Tokens are both used and represented. If you would like to learn more about JSON Web Tokens, you can check out this article by Auth0: An Introduction to JSON Web Tokens.
JSON Web Tokens (JWTs) are great for representing a user's authentication / authorization state without needing to maintain a server-side session store. This makes them great for highly scalable environments since you won't have to connect to a central location to verify the validity of a token. However, this exposes your applications to another problem that isn't exactly trivial either. What do you do when a single token is compromised? Short of invalidating all of your users' tokens by forcing a global key change, there's really nothing you can do. There are, however, some best practices you can follow to ensure your tokens remain secure.
Expiring and Refreshing Tokens
JSON Web Tokens should be used as a temporary representation of a user's credentials. Given their irrevocable nature, JWTs should be given a short time to live, by enforcing a not before time and an expiration time. These values are stored in the payload body of the token. Your implementation should check that the current time is after the not before time, and before the expiration time when verifying a token. By giving a token a maximum time to live, you are reducing the risks associated with a stolen token. For instance, if an attacker compromises a user's token that does not expire, the attacker will have a valid token until you rotate your signing keys. With a short lived token, an attacker would have minimal time to access resources granted by the token.
Now, you may be wondering how your users will authenticate if their access tokens are constantly expiring. For this, we can take a page from oAuth 2's concept of a refresh token. You see, the beauty of a JWT is that it can be verified by anyone immediately, so long as they have the proper keys, without having to reach out a centralized source of truth. A downside of this, is that you relinquish control over that token. Some may counter this by issuing the user an updated token during a request, but there is no guarantee that the user will adopt the new token; after all, their original token is still valid. Here's where refresh tokens come into play. When you authenticate with an oAuth 2 implementation that supports refresh tokens, not only are you given an access token, which expires after a period of time, but you also receive a refresh token. The big difference here is that access tokens do not have the ability to extend their lifespan. You cannot issue another access token from an access token. Access tokens can only be reissued when the refresh token is presented to the provider. We can do the same thing to refresh our JWT tokens too. When a user authenticates using their username and password, you can give them a refresh token to store locally on their machine. Unlike JWTs, refresh tokens should be revokable, this allows authorization to be revoked at any time. By combining these two concepts, we can take advantage of the performance opportunities that come with JWTs, while still maintaining a source of truth that you control.
Choosing a time to live for your tokens can seem daunting at first, but don't worry, you don't have to pick the perfect duration from the beginning. Regardless of the token's time to live, the client implementation will have to do the same thing – request a new access token using the refresh token whenever it's current token has expired. Because of this, you can start with any time to live, and adjust it from there. You can start with a small time frame, maybe five minutes, and increase as time goes on depending on what best suits your environment. If your token refresh process is costly, then you can extend the lifespan of your tokens to reduce the number of refresh requests. If your refresh process does not consume a lot of resources, then you can safely decrease the time to live. The thing to remember here is that you are balancing resources and control. By increasing a token's lifespan, you are reducing your control over the token, and vice versa. A common practice is to issue tokens with a 15 minute time to live.
Choosing the Right Signing Algorithm
When choosing the right signing algorithm, it is important to consider your architecture. What is responsible for creating and signing your tokens? What will be accepting your tokens? Are these the same entity? If the applications consuming your tokens are separate from the applications producing the tokens, chances are you'll want to choose an algorithm that supports public and private keypairs for signing. If you have a single application responsible for both producing and consuming the tokens, then a symmetric key signing algorithm will be sufficient. I've broken down both approaches, but it might be helpful to read both.
Symmetric Signing Algorithms
Symmetric signing algorithms utilize shared keys between your applications. This means that any of your applications can produce valid tokens for your other applications. This is useful if you are building one large application, but if one application can grant access to another application, that could be a security concern. You should use an asymmetric algorithm in this case. The JWT specifications recommend HS256 for symmetric keys. This signing algorithm utilizes HMAC-SHA256 to digest your payload and sign it with a secret key. Your secret key should be a randomly generated 64 bytes for maximum security.
Asymmetric Signing Algorithms
Asymmetric signing algorithms rely on a public and private keypair to sign and verify tokens. This allows you to verify tokens across your applications with the public key, while preventing them from creating new ones. This is useful if a single token can be used across multiple applications to authenticate a user. The JWT specification recommends using the ES256 algorithm for asymmetric signatures. The specification also recommends the RS256 algorithm, but ES256 is on its way to becoming the standard for asymmetric signatures.
Managing your Signing Keys
A cryptoperiod, as defined by the National Institute of Standards and Technology (NIST), is the time span during which a specific key is authorized for use by legitimate entities, or the keys for a given system will remain in effect. In simple terms, a cryptoperiod is the length of time a key can be used before it needs to be changed.
After a key has exceeded it's cryptoperiod, it needs to be rotated in order to keep your environment secure. This also helps ensure that whoever may have access to those keys will only have them for a definite period of time. Key rotation is the process of phasing out old keys, and using new ones. It is a good idea to rotate your keys anytime someone with access to them leaves your organization. Anyone with your keys has the ability to impersonate the service using them.
NIST publishes recommendations on securely managing your cryptographic keys. In this article, I will refer to Sections 5.3 and 5.1 of the Recommendation for Key Management guidelines published in the NIST Special Publication 800-57, Part 1, Revision 4.
If you are using a symmetric key for your JWT signatures, that is considered a Symmetric Authorization Key. If you are using an asymmetric key for your tokens, that is considered a Private/Public Authorization Key. For both types of keys, the NIST guidelines recommend that you rotate the keys at least every 2 years. What this means is that after 2 years you should no longer trust this key; tokens signed by this key are considered no longer valid.
To avoid a period where all of your tokens become invalid, you will need to strategically rotate your keys. In this scenario, where you don't want key rotation to cause any outages, you need to plan ahead. I will also assume that your tokens expire after 15 minutes, but you may have to adjust this for your environment. In order to perform a successful key rollover, your application will need to start signing tokens with your new key pair at least 15 minutes beforehand. This will allow all new tokens generated in that 15 minute window to be valid even after the previous signing keys have been rotated. In this 15 minute window, your applications will also need to be able to see both the old signing key and the current signing key as valid, this means your application will need to be able to support multiple key validation. In doing so, you are ensuring that both sets of keys will be valid. After this 15 minute period, you should then remove the old key from your applications. This will make your old set of keys obsolete. I've also outlined the steps below for readability's sake:
- Generate a new set of keys (either a secret key, or a private/public keypair).
- Configure your application to accept tokens signed by the new key (using either the secret key or the public key), while also keeping your previous key configured.
- Configure your application to sign new tokens using the secret key or private key.
- Wait until all of your previously issued tokens have expired.
- Configure your applications to no longer accept tokens signed by the previous key.
After all is said an done, you will have successfully rotated your keys without causing any issues for your users. This article doesn't go into detail about key storage. Section 6 of the NIST publication details key storage and best practices. Just remember to store your keys securely and give out access to them sparingly. After all, anyone with the key can impersonate your service.
If you're still here, thanks for sticking around. I hope this article has helped you better prepare your environment for handling JSON Web Tokens securely. If nothing else, I hope that you have learned three things from this article:
- Always expire your tokens! Stale, valid, tokens laying around can be dangerous.
- How to choose the right signing algorithm for your environment.
- How to effectively rotate your keys and maintain the security posture of your applications.
Lastly, nobody is perfect. I have tried to reference many credible sources in this article to provide you with accurate best practices and recommendations. If you feel that something should be changed, feel free to comment below or reach out to me via email. Remember, the goal here is to better everyone's security practices; both yours and mine!