Update 15th March 2017: Given recent criticism of the JWT and JOSE specifications, I've written some notes on should you use JWT/JOSE?.
JSON Web Tokens (JWTs, pronounced "jots") are gaining in popularity as a way to securely transmit small packets of information, such as session tokens, proof of identity, and network protocol messages. While there are a great many libraries available that make constructing and verifying JWTs straightforward, it is also easy to misuse them and end up compromising the very security properties you wanted to achieve. In this article, I outline some best practices to ensure that your information stays secure.
It is easy to think of cryptography libraries as a black box. You pick a library, read the docs, and before long you have sprinkled a little digital signature or encryption magic dust on your application and moved on to the next task. This is a mistake. Even with the most carefully designed and implemented library, a lack of knowledge of the security properties that it provides and how it provides them can lead to fatal flaws when used incorrectly.
JWT libraries provide a lot of confusing options:
- Symmetric Message Authentication Codes (MACs) based on HMAC with SHA-256, SHA-384 or SHA-512
- Digital signatures based on RSA or Elliptic Curve DSA.
- Symmetric authenticated encryption based on AES in CBC mode or AES in GCM mode.
- Public key encryption using RSA with various padding modes (PKCS#1.5, OAEP).
- Key agreement protocols such as ECDH or password-based, and various key-wrapping modes.
- Compression of the payload.
These options can be bewildering at first, and you need to pick carefully to get the security properties you desire. For instance, to protect session tokens issued and consumed by the same application, it may be reasonable to use a symmetric encryption mode such as AES-GCM. This provides both confidentiality of the content of the session token and integrity and authenticity to prevent forgeries. However, a public key encryption scheme such as RSA would be wholly inappropriate in this case, as anybody with the public key can then create a valid session token.
Taking some time to learn about the security properties and how the underlying cryptographic primitives provide them will pay dividends in the long run. The Coursera Crypto I course will give you a good start.
With the plethora of modes described in the previous point, it can be tempting to go overboard and use a bit of everything. This is often unnecessary. For instance, if you want to ensure confidentiality and authenticity of your own JWTs then it is is sufficient to use one of the symmetric encryption modes, as these are already authenticated. You can either directly encrypt the JWTs with a shared key, or use one of the key-wrapping modes. It is certainly not necessary to also then apply a separate HMAC, unless you are very paranoid about security margins. (The better idea is to rotate your keys frequently, see below). The more options you use, the more complex your system becomes to reason about and the more credentials you potentially have to manage.
The weakest part of any system's security is often the key management. If a key is compromised then the security properties are no longer guaranteed: messages may be decrypted, security tokens forged, and other parts of your system breached as a result. You should follow best practices for managing your keys:
- Use secure storage for all keys that is not easily accessed in case of a breach. Consider using a Hardware Security Module (HSM) or a dedicated key management service running on a separate host such as Vault or Amazon KMS. Make sure appropriate access and audit controls are in place.
- Rotate keys often, such as once a day or week or month, depending on usage volumes.
- Use a high-quality source of random data for generating keys. Usually this means /dev/random or /dev/urandom on UNIX-like systems, or a dedicated hardware random data generator. If you must use passwords, then use a high-quality password-based key derivation algorithm such as bcrypt, scrypt, Argon2 or PBKDF2. Use high iteration counts (e.g. NIST recommends at least 10,000 iterations for PBKDF2) and unique per-key salts when applicable.
- Immediately rotate any keys suspected of compromise and stop trusting any JWTs using that key.
A JWT consists of a protected payload together with a plaintext "header" section. This can contain various bits of information such as the algorithms used to sign or encrypt the payload or application-specific information to be used by intermediaries on the network, e.g. for message routing. In a lot of cases, this information is redundant and it is downright dangerous to trust its contents anyway. If you do not need to interoperate with third parties that expect standard JWTs, you can save some space and eliminate a whole class of vulnerabilities by simply stripping off the header section when producing a JWT and then recreate it from known data before parsing. I call these "headless JWTs" and recommend you use them wherever you can.
Stripping the header is easy: just remove everything up to the first "." character in the encoded JWT. To reconstruct the JWT, just base64url-encode a fixed header identifying the known algorithm and parameters and prepend it to the headless JWT.
Combining encryption with compression can cause information leaks if you are not careful. This is not just a theoretical vulnerability as practical attacks have been shown. If an attacker is able to control any information in the same encrypted block as some sensitive information, then compression may leak information about the secret parts. Compressing a JWT can be a big win when space is at a premium (e.g. for HTTP cookies), but it should be weighted up against the potential risks. If you need confidentiality and really want compression too, some options to consider include:
- Padding the compressed content to some fixed size that you are happy with. You may still get a decent compression ratio with this.
- Segregate your data into secret information controlled by your application, and non-secret information controlled by user input. Place the secret information into the encrypted & compressed payload and the user-supplied information into the uncompressed (and unencrypted) JWT header. This way the user-controlled information cannot alter the compression of the secret information. If the user-supplied information should also be encrypted, then consider using two separate JWTs.
Where JWTs are used to convey authority, best practice is to limit the period of time that the JWT is valid for. If JWTs must be long-lived, then investigate options for revoking JWTs to prevent further use:
- Blacklisting JWTs based on a unique ID can be made scalable and efficient, but defaults to JWTs being trusted in the absence of information.
- Have short-lived JWTs and require them to be re-issued regularly (e.g. via an OAuth2 refresh token flow). This can be a suitable compromise when the clients are programmatic and so can automatically refresh tokens periodically.
- Use JWTs only to identify resources and some minimal state, while storing the majority of data on the server. The database stores state as normal with a unique identifier (e.g. UUID). The JWT is constructed by the application layer from this unique ID and some minimal metadata. This may seem like the worst of all worlds, but is actually quite a sensible default for a number of reasons: 1. State is kept on the server and can easily be managed, audited and revoked (deleted). 2. Protections in the JWT (e.g. HMAC) allow for a quick check of validity before hitting a central database resource. 3. If tokens are stored in the database but signed/verified by the application layer on-the-fly using a key stored separately, then an attacker must compromise both systems in order to forge or steal tokens. 4. State can be moved from the JWT to the database and vice versa over time as requirements change. 5. Larger amounts of state can be stored in the database than can typically be tolerated in a JWT, and it is much easier to update it.
Like most RFCs, the JWT specs contain Security Considerations sections that detail common threats and advice on how to avoid them. You should read all of these and make sure you understand them before deploying a JWT-based solution:
- RFC 7515: JSON Web Signatures
- RFC 7516: JSON Web Encryption
- RFC 7517: JSON Web Key
- RFC 7518: JSON Web Algorithms
You should also read common criticisms of JWT-based solutions. While I believe that article overplays the risks (there are reasonable mitigations available), it makes some valid points that you should consider before betting the farm on JWTs.
I hope this article has given you some solid advice on building robust and secure JWT-based solutions. When used carefully and with plenty of planning, JWTs can form the basis of highly scalable and reliable systems. By following the best practice in this article, you should be able to put appropriate protections in place to ensure that your data stays secure.
This article was originally published on my consulting company blog: Pando Software