Security of JSON Web Tokens (JWT)
However, improper use of JWT can adversely affect application security. We will give examples of using JWT, analyze common errors in implementing authentication schemes using JWT, consider the main types of attacks on these schemes, and give recommendations on how to prevent them.
JWT format
This section covers the essence of JSON Web Tokens, what they consist of, how they are exploited for user authentication, and what advantages JWT have over the classic session-based authentication schemes.
In accordance with RFC-7519, JSON Web Tokens (JWT) are one of the ways to display data for its transfer between two or more parties as a JSON object.
As a rule, JWT consists of three parts:
- Header
- Payload
- Signature
There are exceptions when JWT lacks a signature. This case will be reviewed later.
Each of the parts — header and payload — is an ordinary JSON object that needs to be additionally encoded using base64url algorithm. Afterwards, the encoded parts are connected with each other and, based on this, a signature is detected that also becomes a part of the token.
Generally, a token looks as follows:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6InVzZXIifQ.ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I
You can see that the token has three parts divided by dots (fig. 1).
Red text is the header:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Initially:
{ "typ": "JWT", "alg": "HS256" }
The purple part is the payload:
eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6InVzZXIifQ
Initially:
{ "id": "1337", "username": "bizone", "iat": 1594209600, "role": "user" }
The blue part is the signature:
ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I
Let us examine the details.
The header is a service part of the token. It helps the application to define how to process the received token.
This part is a JSON object and has the following format:
{ "typ": "JWT", "alg": "HS256" }
Here are the following fields:
- typ — a token type, for example, JWT
- alg — the algorithm used to generate the signature.
The value of the field “typ” is often ignored by applications, however the standards recommend taking it into account to provide backward compatibility.
The “alg” field is obligatory. In this case HS256 (HMAC-SHA256) algorithm has been used in which a single secret key is used to generate and verify the signature.
For JWT signature symmetric encryption/signature algorithms can be used, e.g. RS256 (RSA-SHA256). The standard supports other algorithms, including HS512, RS512, ES256, ES512, none, etc.
The “none” algorithm shows that the token has not been signed. This token does not contain a signature and it is impossible to determine the authenticity of such a token.
The payload carries any information that helps an application to somehow identify the user. Certain service fields can be transferred additionally but none of them are obligatory, and we will not dwell on them.
In our case the payload contains the following JSON object:
{ "id": "1337", "username": "bizone", "iat": 1594209600, "role": "user" }
In this payload there are the following fields:
- id — unique user identifier
- username — name of the user
- iat — a service field, time of token generation in Unix time format;
- role — role of the user. It may contain “admin”, “user”, “guest”, etc.
As the fields in the payload part can be random, the application can use it to store almost any data. For example, the payload can store the user’s full name so as not to request this data every time from the database, and to speed up the application.
The signature is generated as follows:
The signature and payload parts are encrypted by base64url algorithm and afterwards united in a single box using a dot (“.”) as a divider.
Then a signature is generated by HMAC-SHA256 algorithm (in our example) and is added to the initial box after a dot.
On a pseudocode this algorithm looks as follows:
signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), SECRET_KEY ) JWT = base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + base64UrlEncode(signature)
The application, after receiving JWT from the user, calculates the value of the signature and compares it with the value that has been transferred in the token. If these values do not match, it means the token has been modified or generated by an untrusted party and the application will neither accept nor trust such a token.
The signature of the token in the example can be verified using “test” secret key, for example, on jwt.io
Authentication using JWT is quite simple.
A user inserts their login data in the application or a trusted authentication service. In case of successful authentication, the service grants a token to the user containing information about this user (unique identifier, full name, role, etc.).
When further addressing the application, the token is transferred in the user’s requests (in cookies, request headers, post- or get-parameters, etc.).
After receiving the token, the application verifies its signature. Once the application is sure the signature is valid, it extracts the user’s data out of the payload part and authorizes the user.
What are the advantages of using JWT compared with the classic session-based authentication?
First, using tokens makes it possible not to store information about all the issued tokens. When a user addresses the application, they transfers their token. The application verifies the signature and extracts necessary fields out of the payload.
Second, the application does not have to issue and verify tokens by itself, as a separate authentication service is often used for these purposes.
Third, in case of a separate authentication service it is possible to organize a single-entry point to various services with the same login data. After going through the authentication procedure once, the user with the token will be able to get access to those resources that trust this authentication service.
Finally, the application can store almost any data in the payload part that can substantially increase the application efficiency if its architecture is correct.
The factors above resulted in JWT authentication scheme being widely used in various corporate applications. Such a scheme is especially popular in applications using microservice architecture. With this approach, each service gets necessary user data directly from the token and does not waste time on acquiring this information from the database.
Attacks on JWT
This section will review the main attacks on JWT with recommendations on how to prevent such attacks.
User’s token capture may lead to several negative consequences.
First, as JWT is transferred openly, it is enough to apply the base64UrlDecode function to the payload part and receive the initial data stored there. Obviously, a criminal having captured a token, will be able to extract the user’s data stored in the token.
In order to avoid such a threat, the best practice is to:
- Use a secure connection during token transfer
- Never transfer user’s sensitive data in tokens, use only impersonal identifiers.
Second, the criminal having captured a token will be able to reuse this token and access the application on behalf of the user whose JWT has been captured.
The recommendations here will be as follows:
- Like in the first case, to use secure connection during token transfer
- Limit the JWT lifetime and use refresh tokens.
Refresh tokens
In modern authentication schemes based on JWT, the user receives two tokens after authentication:
- access token — JWT based on which the application identifies and authorizes the user
- refresh token — a random token to renew access token.
Access token in this case has a limited lifespan (e.g., 1 minute). Refresh token has a longer lifespan (day, week, month) but it is one-off and serves only to renew the user’s access token.
The authentication scheme in this case looks as follows:
- the user goes through identification and receives the access token and the refresh token from the server;
- when addressing the resource, the user transfers his access token in the request, based on which the server identifies and authorizes the client;
- if the user’s access token expires, the user transfers his refresh token in the request and gets new access token and refresh token from the server;
- if the user’s refresh token expires too, the user must go through the identification process again.
In case of symmetric algorithm for signing JWT (HS256, HS512, etc.) a criminal can try to match the key phrase.
Having done so, the criminal can manipulate the JWT tokens like the application does and therefore can get access to the system on behalf of any registered user.
In our example (see part 1 of the article) a “test” box was used
as the key phrase to sign JWT. This key phrase is simple
and short and can be found in all the main dictionaries
for passwords mining. A criminal can easily match the key phrase using
John the Ripper
or hashcat
.
In this case the recommendations are as follows:
- to use and store the key phrases as confidential information, having considerable length, consisting of upper- and lower-case Latin letters, numbers and special symbols
- to provide periodic change of the key phrase. This will be less convenient for the users, as they will have to go through identification again, but will help to avoid compromising the key information.
As we have already mentioned in the first part of the article, the use of a “none” algorithm in JWT header shows that the token has not been signed. Such a token lacks a part with the signature, and it is impossible to verify the authenticity of that token.
Let us review a similar attack in our example. Our non-coded token looks as follows:
header: { "typ": "JWT", "alg": "HS256" } payload: { "id": "1337", "username": "bizone", "iat": 1594209600, "role": "user" } signature: ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I
Suppose we want the application to regard us as an administrator. We need, therefore, to change the field “role” in payload for “admin”. But if we introduce these changes in the token, its signature will become invalid and the application will not accept such JWT.
To bypass this security mechanism, we can try to change the field “alg” in the token header for “none”. Our token will look as follows:
header: { "typ": "JWT", "alg": "none" } payload: { "id": "1337", "username": "bizone", "iat": 1594209600, "role": "admin" }
Since we are using the “none” algorithm, we do not have a signature to work with. Hence, our encoded JWT will look as follows:
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6ImFkbWluIn0
This token will be sent to the server. A vulnerable application, after checking the JWT header and detecting “alg”: “none”, will accept this token without any verification as if it were legitimate, and as a result we will gain administrator rights.
As methods of precaution against such attacks:
- It is necessary to keep a white list of authorized algorithms on the application side and to dismiss all tokens having a signature algorithm that is different from the one authorized on the server
- It is recommended to work with one algorithm only, e.g., HS256 or RS256.
In case of using asymmetric algorithms for token signature, the signature shall be performed using a private service key and signature verification — using a public service key.
Some libraries used for working with JWT contain logical errors — when receiving a token signed with a symmetric algorithm (e.g., HS256) a public service key will be used as a key phrase for verifying the signature. As a public service key is not secret data, a criminal can easily get it and use for signing own tokens.
To review this example, we will require a new JWT:
header: { "alg": "RS256", "typ": "JWT" } payload: { "id": "1337", "username": "bizone", "iat": 1594209600, "role": "user" } signature: YLOVSKef-paSnnM8P2JLaU2FiS8TbhYqjewLmgRJfCj1Q6rVehAHQ-lABnKoRjlEmHZX-rufHEocDxGUYiGMjMexUQ3zt-WqZITvozJ4pkvbV-mJ1nKj64NmqaR9ZkBWtmF-PHJX50eYjgo9rzLKbVOKYOUa5rDkJPHP3U0aaBXFP39zsGdOTuELv436WXypIZBeRq2yA_mDH13TvzegWCK5sjD4Gh177bCq57tBYjhGIQrDypVe4cWBPlvwFlmG8tdpWGu0uFp0GcbTAfLUlbTSuGROj88BY0XeUs0iqmGlEICES3uqNx7vEmdT5k_AmL436SLedE0VHcyxve5ypQ
When encoded, it will look as follows:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6InVzZXIifQ.YLOVSKef-paSnnM8P2JLaU2FiS8TbhYqjewLmgRJfCj1Q6rVehAHQ-lABnKoRjlEmHZX-rufHEocDxGUYiGMjMexUQ3zt-WqZITvozJ4pkvbV-mJ1nKj64NmqaR9ZkBWtmF-PHJX50eYjgo9rzLKbVOKYOUa5rDkJPHP3U0aaBXFP39zsGdOTuELv436WXypIZBeRq2yA_mDH13TvzegWCK5sjD4Gh177bCq57tBYjhGIQrDypVe4cWBPlvwFlmG8tdpWGu0uFp0GcbTAfLUlbTSuGROj88BY0XeUs0iqmGlEICES3uqNx7vEmdT5k_AmL436SLedE0VHcyxve5ypQ
As in this case we use RS256 algorithm for signature, we will require both public and private keys.
Public key:
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 MwIDAQAB -----END PUBLIC KEY-----
Private key:
-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= -----END RSA PRIVATE KEY-----
For the purpose of testing we are going to utilize jwt.io (fig. 2).
As in previous example, we modify the token:
header: { "typ": "JWT", "alg": "HS256" } payload: { "id": "1337", "username": "bizone", "iat": 1594209600, "role": "admin" }
When encoded, the header and payload look as follows:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6ImFkbWluIn0
We only have to read the signature using a public service key.
To begin, let us transfer the key to hex-representation (fig. 3).
Then we generate a signature using openssl (fig. 4).
We add the value E1R1nWNsO-H7h5WoYCBnm6c1zZy-0hu2VwpWGMVPK2g
to an already existing box, and our token looks as follows:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6ImFkbWluIn0.E1R1nWNsO-H7h5WoYCBnm6c1zZy-0hu2VwpWGMVPK2g
We insert our public key into “secret” on jwt.io, and, as we can see, JWT goes through verification successfully (remember to check the box “secret base64 encoded”!) (fig. 5)
To prevent this attack, we recommend:
- working with only one algorithm, e.g. HS256 or RS256
- selecting well-known and reliable libraries for working with JWT that are less likely to contain logical errors in token verification procedures.
RFC-7515 standard describes “kid” header parameter (Key ID, key identifier). This standard also states that the format of this field is not strictly defined, so the developers can interpret it to their convenience, and this often leads to various mistakes.
Let’s take the following JWT header as an example:
{ "alg" : "HS256", "typ" : "JWT", "kid" : "1337" }
We suppose that for token verification a key with 1337 identifier from the database will be used here. In case of encoding errors this field can be vulnerable to SQL injections:
{ "alg" : "HS256", "typ" : "JWT", "kid" : "1337' union select 'SECRET_KEY' -- 1" }
In this case “SECRET_KEY” box will be used as the key phrase instead of a potential key from the database to verify the key signature.
In the next example we suppose that a key from “keys/service3.key” file will be used to verify the token.
{ "alg" : "HS256", "typ" : "JWT", "kid" : "keys/service3.key" }
There is a possibility that in case a parameter is not validated, a criminal can perform Path Traversal (Directory Traversal) attack, and instead of a potential route to the file he can transfer a route to a public file to the “kid” field:
{ "alg" : "HS256", "typ" : "JWT", "kid" : "../../../images/public/cat.png" }
The criminal can access “cat.png” file and sign JWT using the contents of this file, as this file is public (e.g., published on one of the service pages). The service, having received a route in “kid” field to “cat.png” file uses its contents as a key file to verify the token signature (that will be successful as the criminal has taken care of that beforehand).
A recommendation to prevent such attacks is simple:
- It is necessary to always validate and sanitize the data received from the user even if it has been received as JWT.
Conclusion
JSON Web Tokens are very popular and are highly regarded for their convenience. If used correctly, JWT can prevent errors of inadequate authorization, allow simple and easy distribution of information flows between the servers, organize a single entry-point for various services with the same login data and even increase the service efficiency.
However, if misused, this technology may put entire systems at risk, which may even result in an all-out compromise of the login credentials for all system users.
To conclude, in order to make the JWT use safe and secure, it is recommended to:
- Use secure connection when transferring tokens
- Never transfer users’ sensitive data in the tokens
- Limit JWT lifespan and use “refresh tokens” mechanism
- Use long key phrases
- Keep a white list of authorized signature algorithms on the application side
- Work, ideally, with one signature algorithm only
- Choose well-known and reliable libraries for JWT operation
- Always validate and sanitize the data received from users.