Skip to main content

1. Introduction

JSON Web Tokens (JWTs) are a standard which has become increasingly popular over the last few years, especially for enforcing authentication and authorisation. As with any newer piece of technology, developers have been quick to update their systems or use them for new projects. Although JWTs have security controls as built-in features, simply importing a library is not sufficient for ensuring that your implementation is secure. Instead, there comes new security considerations which should be applied. This blog post aims to explain JWT’s features, attack surface and security best practices.

What are JWTs?

Let’s start with a hypothetical situation; you have a single authentication server that grants users access to various other systems. You want users to be able to log in to the authentication server and be provided with a proof of identity which they can use to access various other resource servers. There’s a lot of overhead here; the authentication server needs to generate a session after being supplied with valid credentials, and it also needs to inform the other servers of this so that users can be afforded access. Alternatively, each resource server would need to ask the authentication server to validate the supplied session identifier when a user tries to access a resource. So, how can we keep this system architecture but reduce the amount of communication and coordination required?

Figure 1: Inter-server communication required

This is where the JSON Web Token (JWT) comes in. It was proposed in RFC 75191, which defines a method of transferring information (referred to as claims) between two parties, with controls to enforce integrity. Integrity refers to ensuring that the claims have not been tampered with after they have been issued. Claims are usually transferred in JavaScript Object Notation (JSON) format (other formats, such as XML, could also be used), which has the benefits of being human readable, lightweight and easily parsed.

So how can JWTs be used to solve the above problem? JWTs are issued to the client, and stored locally. They are then forwarded with a client’s request to access a resource. The resource server can verify that it has not been tampered with using the built-in integrity controls, and use the claims presented within it to either grant or deny access to a resource. The ability for the JWT to provide integrity controls means that the individual servers do not need to keep a record of the user; they simply verify that the token was actually generated by the authentication server. This is a substantial benefit; the server receiving the token does not need to store any information relating to the user and verifying the token replaces the inter-server communication which would otherwise be required. This also allows an architecture making use of JWTs to be highly scalable as additional resource servers are added.

Figure 2: Resource servers validating and accepting a JWT

There are a multitude of other interlinked RFCs related to JWTs. For simplicity’s sake, we’ll only define how JWS and JWE fit in:

  • JWS (JSON Web Signature)2– defines the signature used to enforce integrity and prevent tokens from
    being tampered with.
  • JWE (JSON Web Encryption)3 – defines a method of encrypting the payload claims in order to enforce
    confidentiality of information.

A traditional JWT makes use of JWS to enforce integrity, but does not implement JWE to encrypt the information stored within it. JWTs are usually used for authorisation between a client and server (such as a RESTful API including the token in an Authorization Bearer scheme), but they can also be used for transferring other information. For example, a JWT could be crafted which contains the relevant information for
a banking transaction to be processed. A transaction server receiving the JWT could verify its integrity and perform the relevant transaction. If there is sensitive information stored in the token, a JWE may be used in order to encrypt the information. There’s a multitude of use cases for JWT implementations and the claims stored within them are highly customisable.

Of course, increased complexity and customisability comes with increased security considerations. We’ll dive into that after learning a little bit more about how JWTs work.

2. Structure

Now that we know what a JWT is, let’s take a look at its structure. JWTs consist of three components:

  • header
  • payload
  • signature

Each of these sections are separated by a full stop. An example of a simple JWT, used for authorisation purposes, is shown below:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJpc3MiOiJtd3IiLCJ1c2VybmFtZSI6ImpvbmF0aG
FubGV5bGFuZCIsInJvbGUiOiJyZWFkb25seSIsImV4cCI6MTY1NjI2MDI2NH0 .DgfR7jHdSDBG20ydC
4VT4sqA7ycUEcLgKnlgCAQA2gA

As you can see, the above format is clearly not human readable. This is due to the fact that the header, payload and signature (the last one is optional) are base64url encoded. Encoding is a method of converting data from one representation to another, and anyone with knowledge of the encoding format could decode and reveal the contents of the message. This makes encoding distinct from encryption, the latter of which requires a piece of secret information (such as a password or private key) in order to decrypt a message’s contents. Base64url is a subset of the popular Base64 encoding format – the only difference being that base64url never results in the following characters within its output: ”+”, ”/” and ”=”. These characters have special meanings in URLs; thus base64url encoding allows JWTs to be used safely within them (which should probably be avoided regardless – more on this later).

By taking the header, payload, signature, and base64url decoding each of them interdependently we can see the following output. You can also copy and paste the token into jwt.io’s interface4.

Header:
{
    "alg": "HS256",
    "typ": "JWT"
}
Payload:
{
    "iss": "mwr"
    "username": "jonathanleyland",
    "role": "readonly"
    "exp": 1656260264
}
Signature:
i0tGXCwn_oHnjA1KtPEkd5c6N44KzEGqvWgvwZxifvs

Within the header and payload, different types of information (made up of a name and value pair) can be defined. The header contains header parameters whereas the payload contains the claims mentioned previously. The parameter ”alg” is defined with a value of ”HS256” within the header, specifying the algorithm which was used when generating the token’s signature. Additionally, the optional ”typ” parameter specifies that this object constitutes a JWT.

The payload contains the claims which a client has been granted by the authentication server. Claims fall into one of the following three groups:

  • Registered – defined by the JWT specification.
  • Public – created by anyone using JWTs and defined in a public registry to avoid naming collisions – these are custom registered claims and would typically have special meanings.
  • Private – defined based on the information requirements of a specific system and do not need to be registered publicly.

In our example, we can see that the Registered ”iss” claim is used; this claim is used to specify the server that issued the token. Two private claims are also defined: ”username” and ”role”. This information is used to inform a resource server who is performing a request as well as their assigned permissions. Finally, the ”exp” (expiry) claim allows the receiving server to accept or reject tokens based on whether or not they are still active.

As we’ve seen thus far, the JSON contents can be viewed by base64url decoding it. So what’s to stop an attacker from taking the ”role” defined above and modifying its value to something such as ”administrator”? This is where the signature comes into play.

The signature ensures that the claims defined in the header and payload have not been tampered with; ensuring the integrity of the token. When a token is generated and issued, the contents of the header and payload are signed using the algorithm defined in the header’s ”alg” parameter, using a secret signing key only known to the authentication and resource servers. The result is a unique signature which can only be generated with the specified header, payload and secret key. The signature is generated using hashing, a one-way transformation; meaning that there is no way to retrieve the original information that was hashed. On the receiving end, a supplied token can be hashed and the resulting signature validated against the supplied signature to ensure that it came from the issuing server. As the user does not know the secret key, they cannot forge their own signatures.

Figure 3: Received signature verified against signature generated using a secret

So that’s a quick look at how JWTs are structured. Note that this was just one example of how a JWT can be defined; however, regardless of how they are structured, they all follow the same principle of signature validation. So with that out the way, lets dive into the world of attacking and securing JWTs!

3. Security Considerations

Sensitive Information in JWTs

As mentioned earlier, the contents of a standard JWT can be decoded by anyone due to the use of encoding and its easily recognisable format. As such, there should be no sensitive information stored in the header or payload. In the event that a JWT is stolen, either through direct access to a user’s machine or through another vulnerability (such as cleartext communications), the presence of sensitive information such as a password (an extreme example) would significantly increase the impact of such an attack. For example, an attacker with this information could modify the victim’s password to gain control of their account or use it to generate additional JWTs through the authentication server rather than being constrained to using the stolen token for the duration of its active lifetime.

In certain situations and use cases it could be necessary to transport sensitive information within the JWT. One such situation would be to allow sensitive information to be passed from one resource server to another via the user. This can be resolved through the use of a JWE mentioned previously, which encrypts the content stored in the token’s payload. This ensures that the confidentiality of the information can be preserved as well. Even if an attacker were able to steal the token they would not be able to decrypt the payload without additional flaws being present. We’ll come back to JWE a bit later.

JWT Storage and Transport

Although JWTs are URL-safe due to the use of base64url encoding, they should not be sent as a parameter within the URL as this could lead to the JWT being leaked in a variety of ways, including the following possibilities:

  • Users sharing links which contain their JWT.
  • JWT being stored in accessible server or client-side logs.
  • JWT leakage through the browser’s history.
  • JWT being leaked to 3rd party websites, such as Content Delivery Network (CDN) servers, via the Referer header.

There are alternative methods of forwarding the token to the server which should be considered instead. Each of these methods has their own security considerations as well.

The first method of sending the JWT is to use a hardened cookie. This entails storing the token in a cookie with all of the relevant security flags set, such as the HttpOnly and Secure flags. It should be noted that using a cookie could allow the client to be vulnerable to Cross-Site Request Forgery5 (CSRF) attacks. As such, suitable controls such as CSRF tokens and origin verification should be implemented.

An alternative is to store the JWT either in sessionStorage or localStorage in the browser. sessionStorage is recommended due to the fact that information there is removed once the browser’s page session is terminated. The JWT can then be added by the application to the Authorization header using Bearer Authentication. However, the use of either of these storage techniques could leave the JWT susceptible to being stolen in the event of a Cross-Site Scripting (XSS) attack, as both of these storage locations are accessible via JavaScript. As a result, there are tradeoffs to both methods.

It could be possible to mitigate the impact of a stolen token through the use of fingerprinting; whereby the server tracks a supplied value in the token (such as the registered ”jti” claim which represents a unique JWT identifier) versus a server-side blocklist that rejects the token after its initial use. However, this paradigm adds to server-side processing and would only be useful in the instance that single-use tokens are suitable for your implementation.

Lack of Signature Verification

The signature verification process is vital for ensuring that the JWT has not been tampered with. Without it, an attacker could simply choose the values included within claims along with an arbitrary signature that does not get validated. For example, an attacker could modify their assigned user role in the above example (”readonly”) to a role with increased permissions, such as ”administrator”.

If a JWT library is used, the requirement for signature verification to occur should be explicitly stated within each function. For example, the [JWTAuthentication] attribute in the following ASP.NET Core code block ensures that the JWT is validated before the function can be called.

Alternatively, if anonymous access should be allowed, the [AllowAnonymous] attribute specifies it in this example.

None Hashing Algorithm

One of the more well-known attacks targeting the signature verification process involves performing a client-side modification of the value of the ”alg” header parameter. By changing the value to ”none”, it could be possible to direct the implementation of the JWT library to skip signature verification completely. This is a case of the library accepting information supplied by the client rather than enforcing its own rules. If this attack is successful, it would be trivial to forge tokens that would be accepted by the system, leading to fraudulent information supplied by the client being deemed as valid. The server should be aware of its own signature verification algorithms, and it should enforce them rather than allow the client to supply it.

You might be wondering why an RFC would implement a ”none” algorithm in the first place? The none algorithm exists for instances where signature verification is no longer required. For example, if a token is validated at an API Gateway and forwarded to internal micro-services then the signature could be stripped to increase speed and minimise resource consumption. The signature doesn’t need to be validated by each service consuming the token as it has already been processed.

RS256/HS256 Confusion Attack

In a similar manner to providing a ”none” algorithm, additional attacks are sometimes possible when the server can be coerced into using a different signing algorithm than the one it expects. Symmetric algorithms (such as the commonly used HS256 algorithm) use a secret key, only known to the authentication and resource servers, during the hashing process. Alternatively, signatures can be created using asymmetric algorithms such as RS256. In the second instance, the authentication server uses a private key to create signatures and a public key can be utilised by resource servers to verify the token has originated from the correct source.

Figure 4: Asymmetric signature validation

In some implementations of the JWT standard, an attack was possible whereby an attacker could modify the ”alg” header parameter, this time from an RSA (i.e. RS256) algorithm to an HMAC (i.e. HS256) algorithm. This change from asymmetric to symmetric would require that a secret key is not used to perform signature verification. Due to how the verification protocol is handled, some implementations would default to treating the RSA public key as the HMAC secret key. This causes a problem as public keys are, by definition, public. As such, an attacker could make use of the public key (if accessible) to forge HMAC signatures which would be accepted as valid by the server6.

As mentioned previously, the server should be aware of the expected signing algorithm and its allowed variations. Unexpected algorithms should be rejected through the use of an allowlist before the verification process takes place.

An additional form of confusion attack exists where, by inserting a public key into a forged JWT, some libraries may use the included public key to verify the signature. This could allow an attacker to forge valid tokens using their own private key. This is quite a specific case for some libraries, and more information is available in CVE-2018-01147.

To conclude this section, these problems boil down to trusting client-side information rather than enforcing the expected protocol. For RS256/HS256 confusion attack, it also points to the protocol not handling unexpected input in a safe manner. The key takeaways here are to enforce rules server-side and reject any ambiguous tokens that are supplied.

Brute Forcing the Secret Key

Symmetric algorithms make use of a secret key, which is similar to a password as opposed to a certificate, in order to perform hashing. An attacker wishing to forge a JWT would need the secret key in order to create a hash that would be accepted. As JWTs are issued to the client, it is possible to perform an offline brute-force attack against the secret key value. An attacker can use their initial token to validate whether or not a guessed secret key was correct or not. Once a matching hash is identified, an attacker could leverage the knowledge to forge arbitrary tokens. This is often seen when a JWT implementation is copied from a blog post – a developer could set a default key value without realising the importance of using a secure value.

Tools such as jwtcat8 can be used to automate the cracking process, which would be highly effective if a weak secret was used.

If a secret key is employed, it is recommended that the entropy of the secret key is sufficient to make brute-force attacks completely unfeasible. The secret key used to verify signatures should be at a minimum as many bits as the hash produced by the algorithm. For example, HS256 creates a 256 bit hash, thus the secret key should be 256 bits at a minimum. The key should be generated with a secure cryptographic library, ensuring sufficient randomness.

Lack of Logout Functionality

We’ve seen that a major benefit of JWT is that it allows the backend server to be stateless; all information is stored by the client and the signature verification process ensures that the claims contained within it are valid. More traditional approaches, such as the use of a session identifier stored in a cookie, allow the client to force a logout by clearing the cookie client-side and destroying any references to it server-side. Clearing a cookie storing a JWT is not helpful, as there is no server-side information to destroy and placing it back into local storage manually would allow continued use of the JWT. This is not necessarily a problem – it’s an inherent feature of JWT.

Instead, JWTs have a timeout feature and are considered invalid after a set period of time. Registered claims such as the ”iat” (issued at) claim can be used to create a generation date/time such that the JWT can be rejected after the active amount of time has expired. Due to this, the expiration time should be sufficiently short in order to limit the window of opportunity in which a stolen JWT can be used. In order to prevent inconvenience to users, a refresher token pattern can be used to generate a new token once the current one has expired – more on this later.

However, what if you want to have the ability to revoke a JWT if necessary? A server-side blocklist could be kept in a database to track tokens which are still active according to their timeout but have been manually revoked. Received tokens can be compared against this blocklist before verification takes place. Arguably, this defeats the purpose of using JWTs – it is supposed to leverage the fact that information does not need to be stored server-side at all. This is an important factor to consider when choosing an authentication model.

Injection Attacks

If the value of a claim can be controlled by the user, such as through token forgery or it being set by the user during an onboarding process for example, injection attacks9 could be possible if claim values are not handled safely. A simple example could be that users specify their username when creating an account, and that the username is included in a JWT claim, later used for database queries. If an attacker were able to inject a malicious payload into their username and database queries were built unsafely, a SQL injection attack could be possible. This extends to other types of injection attacks, such as path traversal for file handling operations and command injection for claims used in OS functions.

Strict input validation should be performed on all parameters which will be used as claims within the JWT. In addition, best practices to prevent injection attacks (such as parameterised queries in the case of SQL injection) should be applied when inserting user input into code. Finally, the JWT should have its signature verified before any queries are made, restricting the ability for attacks to send arbitrary payloads for processing.

Replay attacks

A replay attack occurs when an attacker is able to steal a JWT and use it in order to repeat a user action using the token, which would be applicable when a JWT is used for a transaction for example. The optional ”jti” (JWT ID), which provides a unique identifier for JWTs, could be used to prevent these types of attacks. Once used to perform the specific request or transaction, the ”jti” value can be added to a server-side blocklist ; preventing the token from being used to perform that same action again. A private claim could also be defined to achieve the same function. Setting a short timeout period can also be used to help mitigate this issue.

Framework specific vulnerabilities

A number of the issues described above can be resolved by following security best practices and using a wellknown and trusted JWT library to handle JWT processing; however, it should be noted that flaws can exist within these libraries as well. As such, a strict patching policy should be in place to ensure that newly discovered vulnerabilities are remediated by updates. Vulnerability disclosure sites should also be monitored to assess any potential risks such that they can be mitigated in lieu of a patch.

For example, Tim Mclean revealed the algorithm confusion attacks in certain libraries discussed previously in a blog post on Auth0’s website10. In the absence of applying patches, these vulnerabilities could still be present in systems today. The jwt.io11 website can be used as a useful resource for further information on the different libraries available, their supported algorithms and their ability to verify registered claims. Using a library which supports verification of the required registered claims would not only decrease development time, but also reduce the risk of custom implementations being vulnerable.

That’s it for the vulnerability side of things; however, we’re not done just yet! There are further concepts which should be understood when developing or attacking JWTs.

4. Further Concepts

The following concepts are helpful, either for implementing controls for the above vulnerabilities or for customising a token to fit the needs of a system.

Registered Claims

There are various optional claims which can be defined as a part of the JWT specification. These are useful for defining additional security rules which can be enforced by the servers receiving tokens.

As mentioned previously, the JWT specification does not allow explicit token revocation without additional logic being added server-side. As such, JWTs are usually valid until they time out. The timeout period must be enforced by each server receiving tokens. The expiration time can be validated by checking registered claims such as ”exp”, which defines an expiration date, and ”iat”, which defines the date the token was issued. Either can be used to ensure that tokens timeout properly and thereafter are not accepted by the server. The ”nbf” (not before) claim can also be specified which decides the time after which a token should be considered active.

The ”iss” (issuer) claim can be used to identify the system which generated the JWT. It could be useful to validate this value in the case where multiple systems are responsible for issuing JWTs. The ”aud” (audience) claim specifies the recipient of the JWT. For example a JWT could be issued by a centralised system (documented in the ”iss” claim), and the ”aud” claim could define which application the JWT grants access to. The audience claim should be validated, and if the value provided does not match the value associated with the receiving application then the JWT should be rejected.

The ”sub” (subject) claim refers to the user who the JWT has been issued to. It could be used to perform authorisation checks when accessing resources or to pull out other relevant information related to the user. It should be a unique value assigned to each user of the system. In this example, the subject claim would replace a private ”username” claim.

Where relevant, claims defined in the implementation should be verified against the expected value. In a similar manner to the hashing algorithm used, although tokens are stored client-side, it should not be up to the user to define the value used within them.

Key Management

There are various other claims which can be defined that can be used to help with key management. A few examples are listed below:

  • ”kid” – The key identifier claim can be used to specify which key should be used to validate the signature of the supplied JWT. The use of this claim could introduce directory traversal or injection attacks to bypass the signature verification mechanism, so implement it carefully!
  • ”jwk” – This specifies the public key which corresponds to the private key which signs the token.
  • ”jku” – JWK Set URL provides a link to a set of public keys, one of which corresponds to the secret key used to sign the token. The URL to access this information should be suitably protected using Transport Layer Security (TLS).

There are more examples, and all are optional to be used within an implementation. The key point to remember here is to always perform validation. The values provided to the system should match the expected values, and should not be blindly accepted and used. For example, an attacker could set the ”jku” to a URL controlled by them. If this value is not enforced using an allowlist, it could be possible for an attacker to forge tokens by specifying their own public key for signature verification.

Sometimes it is recommended that the keys are rotated on a regular basis. Key rotation is not considered necessary unless a breach occurs. There should be suitable policies in place in order to assign a new key for the JWT in the event that a breach does occur; however, changing keys without a valid reason for doing so does not provide additional security.

JWE

As discussed previously, JWTs are not suitable for sending private information due to the possibility of the token being intercepted and any sensitive information decoded. JSON Web Encryption (JWE) allows the payload of the JWT to be encrypted; meaning that sensitive information can be sent safely between clients and servers. This ensures the confidentiality of information. In addition, the signature scheme is still used which adds the benefits of integrity validation of tokens.

Note that a JWE consists of five rather than three components. Therefore, if it looks like a JWT but appears too long or is separated by additional full stops, you probably have a JWE on your hands.

In a similar manner to the signing process, encrypting the payload can use either asymmetric or symmetric encryption. In the case that symmetric encryption is used, the same key length policies described previously should be applied. In addition, there should also be a suitable method of updating either the secret key or private and public key pairs in the event that a breach occurs.

JWE implementations have been found to be vulnerable to some notable vulnerabilities such as the classic invalid curve attack12. Exploiting this vulnerability could allow an attacker to extract the secret key and thus forge tokens. As before, standard libraries that have the latest patches applied should be used to ensure that the implementation is thoroughly tested and secure.

Refresher tokens

Remember when we mentioned that refresher tokens can be used as a solution for usability with short-life tokens? Previously, it was recommended that the timeout period for JWTs is kept as short as possible. This could cause a usability issue whereby users need to re-authenticate to the token granting server repeatedly to maintain access to resource servers.

To solve this problem, some implementations make use of the refresher token pattern. Refresher tokens can be used to grant additional JWTs without prompting the user to re-authenticate. When a JWT times out, the refresher token is forwarded to the authentication server and a new JWT is issued to the user.

Figure 5: Refresher token granting an additional JWT

This can happen in the background, without direct user interaction. Refresher tokens have a longer lifespan than JWTs and should be constrained to the same strict storage requirements. If an attacker were able to retrieve a refresher token, they would be in a position to generate additional JWTs and extend their access beyond the window of opportunity granted by a normal JWT. Server-side logic should also be in place in order to revoke refresher tokens when a user has terminated their requirement to generate additional JWTs.

5. Conclusion

As we’ve seen, there are a number of security considerations which should be kept in mind to ensure that a JWT implementation is robust and secure. To summarise the above points, some key rules need to be applied. If you’re developing a JWT solution, remember to:

  • Not store sensitive information in unencrypted JWTs
  • Perform server-side validation of the signature, using the expected algorithm
  • Enforce static claim values server-side using an allowlist
  • Make use of cryptographic best practices when choosing algorithms and secret keys
  • Treat all user input stored in the JWT as untrusted and perform the required input validation steps
  • Make use of registered claims which are helpful to your implementation and perform additional checks on the supplied values
  • Ensure that a sufficiently short timeout is defined and enforced
  • Don’t reinvent the wheel – make use of standardised libraries and apply patches regularly

If you’re testing a JWT implementation for security vulnerabilities, start by trying to exploit the examples listed above. Evaluate how the server processes unexpected input, and see if that could lead to the ultimate goal; forging your own tokens that would be accepted by the server.

Finally, make sure that you are using JWTs because they fit your use case. There are a variety of benefits to using them, but that does not necessarily mean that they are ideal in certain situations. In the case of a single server interacting with users, it could be the case that traditional session identifiers are better due to their standardisation within frameworks and the built-in ability to terminate sessions. JWTs are a great standard, but should not be used simply for the sake of keeping up with the latest technology trends.

6. Further Reading

  • Want to try some of these attacks yourself? Check out the JWT attacks13 module from Portswigger’s Web Security Academy.
  • Looking for security implementation examples? Have a look at OWASP’s JSON Web Token Cheat Sheet14.
  • Just want to play around, generate your own tokens and see what libraries are available? See jwt.io15.