JSON Web tokens (JWT): how to use them safely
A few weeks ago some colleagues from a development team told us about their worries on the JSON Web tokens (JWT) generation they were doing as part of a new tool integration they were working on. They had heard about several security issues regarding the use of JWT tokens so they asked us for help in order to validate if the tokens they were issuing were correct and met some basic security requirements.
We are currently working on a project to help automate security tests, APICheck, which we’ve recently released as open source. APICheck is comprised of a set of small tools that can be pipe chained in order to run several tests on API requests, so we got down to work with the development of a new tool for validating the JWT they were issuing, jwt-checker, in which we’ve implemented the ability to pass the validations on the tokens we’ll talk about. Below I’ll show you an example of a test implemented using the tool.
Are JWT tokens secure?
JWT (JSON Web Token) is an open standard (published in the RFC 7519) which defines a compact and self-contained method to encapsulate and share assertions (claims) about an entity (subject) between peers in a secure manner by using JSON objects. The content inside the token can be trusted and verified because it’s digitally signed (JWS, RFC 7515). The signature can be generated by using both symmetric (HMAC algorithms) or asymmetric keys (RSA or ECDSA). Additionally JWT can carry encrypted data (JWE, RFC 7516) to protect sensitive data, although we won’t see it in this study.
It is worth noting that by default JWT are not encrypted, and that the string we see is simply a base64url encoded serialization that can be easily decoded to see the plain JSON content that the token carries.
So the response to the initial question is ‘It depends...’. As with many other technologies, JWT depends heavily on a good configuration when issuing the tokens and in a correct use and proper validation of the consumed tokens.
JWT is an open standard which defines a compact and self-contained method to encapsulate and share assertions about an entity between peers in a secure manner by using JSON objects.
Types of JSON Web Tokens and use cases
We’ll start by seeing what the main types of tokens are and the most important use cases.
- Data token: As the JWT serialized form is compact and easy to integrate in HTTP request JWT are often used as a mechanism of data interchange.
- ID token: Issued by an Identity Manager, on behalf of a client application, after authenticating the user. It allows the client application to get user information from the token in a safe way without the need of managing user credentials.
- Access token: Issued by an authorization server, on behalf of a client application, it allows the client application to access a protected resource on behalf of a user. This kind of token is used as an authentication and authorization mechanism by the client application towards the server holding the resource.
JWT allow for interchange of data between peers in a more performant way than other standards (SAML) due to its smaller size and ease of parsing. This is what makes them ideal for the following use cases:
- Session data interchange between client and server: JWT are sometimes used to transmit GUI state and session information between the server and its clients. Usually they are unsecured tokens (without a signature).
- Federated authentication: It eliminates the need for applications to manage their user credentials, by delegating the process of user authentication to an identity provider. The provider generates a token, that is verifiable by the application, and that contains the data needed about the user.
- Access authorization: The token contains the information needed by an API server to decide if the operation requested by the token holder can be carried out.
Each use case has different recipients (client application and API service), but in the case that you maintain control over both the application and the API service you can use a single token to address both authentication and authorization.
Next we are going to enumerate the best practices when working with JWT, focusing only in generation and validation processes.
Issuing a token
Always sign the token
Except in very few cases (when used in the client side, for carrying GUI state data and session information) a token must not be issued without a signature. The Signature is a basic protection that allows token consumers to trust it and to ensure that it has not been tampered with.
Use strong cryptography
When choosing the signing algorithm one thing to be taken into account is that the symmetric key algorithms are vulnerable to brute force attacks if the key isn’t strong enough (pet names and birthdays are useless here too ;-), so you must assure enough entropy for the key if you choose a symmetric key algorithm. On the other side asymmetric signing algorithms simplify the key custody, because the latter is only necessary on the server side issuing the token.
Set expiration date and unique identifier
A JWT, once signed, is valid forever if no expiration date was given (claim exp). For Access tokens, anybody capturing the token will have access to the granted operations forever. Assigning identifiers (claim jti) to tokens allows for their revocation; in the case the token is compromised it is very helpful to have the choice of revoking the token.
Set the issuer and audience
In order to ease the management of the tokens to the recipients it is mandatory to identify the issuer (iss claim) and all possible recipients (audience claim, aud); with this information it will be easy for them to locate the signature key and to ensure that the token was issued for them. It is also a best practice for recipients to validate these claims.
Don’t include sensitive data unless you encrypt the payload
As we said above, JWT are not encrypted by default, so care must be taken with the information included inside the token. If you need to include sensitive information inside a token, then encrypted JWT must be used.
As an example, here is the execution of a test that checks some of the validations we’ve seen using jwt-checker:
$ docker run --rm bbvalabs/apicheck-curl http://my-company.com/auth/ | \
docker run --rm -i bbvalabs/jwt-checker -allowAlg HS256 -allowAlg HS384 \
-issuer bbva-iam -audience my-api-id -expiresAt 20200520T20:15:00\
-secret bXlTZWNyZXRQYXNzd29yZG15U2VjcmV0UGFzc3dvcmQK
In this example we can see one of the capabilities of APICheck, this is the chaining of actions. In this case the first tool makes a request in order to obtain a token, generating an interchange object that is passed on to the following tool, our validator in this case, which checks several of the aspects we’ve seen.
Validating a token
Don’t accept unsigned tokens
The signature is the only way to verify that the data contained inside the token has not been tampered with. So the second validation we have to do, after validating the token format, is to check that it has a signature. This option must always be active to avoid the case where an attacker could intercept the token, remove the signature, modify the data and resend it. Don’t ever accept tokens with ‘alg: "none"’ in its header. The best protection is to always validate that the alg claim contains a value from a set of expected values, the smaller the set the better.
Validate header claims
You must never trust the received claims, especially if we are going to use them for searches in backends. For example kid claim (key identifier) can be used to perform the signing key lookup, so we must sanitize its value to avoid SQL injection attacks. Other examples are the use of the jku (URL to a JWK Set) and x5u (URL to a X.509 certification chain) fields, that can contain arbitrary urls and cause SSRF attacks if used without proper validation, for example by using a whitelist of allowed URLs.
Always validate issuer and audience
Before accepting a JWT we must verify that the token was issued by the expected entity (iss claim) and that it was issued for us (aud claim); this will reduce the risk of an attacker using a token, intended for another recipient, to gain access to our resources.
Index stored keys by issuer and algorithm
When looking up the signing key we must check that the signing algorithm is valid for the issuer. An attacker could intercept a token using an RS256 algorithm, modify it and create a signature using the public key of the issuer (which could be easily found) by using a HS256 algorithm. If we do not check this point we’ll accept the token as valid when it really is not.