Logo

Tokens

Components Of JWTs Explained

By Dan Moore

Components Of JWTs Explained

A JSON Web Token (commonly shortened to JWT), is a token typically used with standard protocols such as OAuth2. This article explains the component parts of a JWT, and helps explain how the JWT works.

Before we continue, it’s important to note that JWTs are often mistakenly referred to as JWT Tokens. Adding the Token on the end would expand to JSON Web Token Token. Therefore, we leave off the trailing Token and simply use JWT in this article as it is the more correct name. Likewise, because JWTs are often used as part of an authentication and authorization process, some people refer to them as Authentication Tokens or JWT Authentication Tokens. Technically, a JWT is merely a token that contains Base64 encoded JSON. It can be used in many different use cases including authentication and authorization. Therefore, we don’t use this term in this article either, but we discuss how JWTs are used during the authentication process.

Let’s dig in! Here’s a freshly minted JWT. Newlines have been added for clarity, but they are typically not present.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImY1ODg5MGQxOSJ9.eyJhdWQiO
iI4NWEwMzg2Ny1kY2NmLTQ4ODItYWRkZS0xYTc5YWVlYzUwZGYiLCJleHAiOjE2NDQ4ODQ
xODUsImlhdCI6MTY0NDg4MDU4NSwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiIwMDAwMDAwM
C0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDEiLCJqdGkiOiIzZGQ2NDM0ZC03OWE5LTR
kMTUtOThiNS03YjUxZGJiMmNkMzEiLCJhdXRoZW50aWNhdGlvblR5cGUiOiJQQVNTV09SR
CIsImVtYWlsIjoiYWRtaW5AZnVzaW9uYXV0aC5pbyIsImVtYWlsX3ZlcmlmaWVkIjp0cnV
lLCJhcHBsaWNhdGlvbklkIjoiODVhMDM4NjctZGNjZi00ODgyLWFkZGUtMWE3OWFlZWM1M
GRmIiwicm9sZXMiOlsiY2VvIl19.dee-Ke6RzR0G9avaLNRZf1GUCDfe8Zbk9L2c7yaqKME

This may look like a lot of gibberish, but as you learn more about JWTs and how they are used in OAuth2 or authentication processes, it begins to make more sense.

There are a few types of JSON Web Tokens, but I’ll focus on signed JWTs as they are the most common. A signed JWT may also be called a JWS. It has three parts, separated by periods.

There’s a header, which in the case of the JWT above, starts with eyJhbGc. Then there is a body or payload, which above starts with eyJhdWQ. Finally, there is a signature, which starts with dee-K in the example JWT.

How does a JWT work? Let’s break this example JWT apart and dig a bit deeper.

The JWT Header Explained

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImY1ODg5MGQxOSJ9 is the header of the JWT. The JWT header contains metadata about a JWT, including the key identifier, what algorithm was used to sign in and other information.

If you run the above header through a base64 decoder:

echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImY1ODg5MGQxOSJ9'|base64 -d

You will see this JSON:

{"alg":"HS256","typ":"JWT","kid":"f58890d19"}%   

HS256 indicates that the JWT was signed with a symmetric algorithm, specifically HMAC using SHA-256.

The list of algorithms and implementation support level is below.

“alg” Param ValueDigital Signature or MAC AlgorithmImplementation Requirements
HS256HMAC using SHA-256Required
HS384HMAC using SHA-384Optional
HS512HMAC using SHA-512Optional
RS256RSASSA-PKCS1-v1_5 using SHA-256Recommended
RS384RSASSA-PKCS1-v1_5 using SHA-384Optional
RS512RSASSA-PKCS1-v1_5 using SHA-512Optional
ES256ECDSA using P-256 and SHA-256Recommended+
ES384ECDSA using P-384 and SHA-384Optional
ES512ECDSA using P-521 and SHA-512Optional
PS256RSASSA-PSS using SHA-256 and MGF1 with SHA-256Optional
PS384RSASSA-PSS using SHA-384 and MGF1 with SHA-384Optional
PS512RSASSA-PSS using SHA-512 and MGF1 with SHA-512Optional
noneNo digital signature or MAC performedOptional

This table is drawn from RFC 7518. As only HS256 is required to be compliant with the spec, consult the software or library used to create JWTs for details on supported algorithms.

Other metadata is also stored in this part of the JWT. The typ header indicates the type of the JWT. In this case, the value is JWT, but other values are valid. For instance, if the JWT conforms to RFC 9068, it may have the value at+JWT indicating it is an access token.

The kid value indicates what key was used to sign the JWT. For a symmetric key the kid could be used to look up a value in a secrets vault. For an asymmetric signing algorithm, this value lets the consumer of a JWT look up the correct public key corresponding to the private key which signed this JWT. Processing this value correctly is critical to signature verification and the integrity of the JWT payload.

Typically, you’ll offload most of the processing of header values to a library. There are plenty of good open source JWT processing libraries. You should understand the values, but probably won’t have to implement the actual processing.

The JWT Token Body

The payload, or body, is where things get interesting. This section contains the data that this JWT was created to transport. If the JWT, for instance, represents a user authorized to access certain data or functionality, the payload contains user data such as roles or other authorization info.

Here’s the payload from the example JWT:

eyJhdWQiOiI4NWEwMzg2Ny1kY2NmLTQ4ODItYWRkZS0xYTc5YWVlYzUwZGYiLCJleHAiOjE2NDQ4ODQxODUsImlhdCI6MTY0NDg4MDU4NSwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDEiLCJqdGkiOiIzZGQ2NDM0ZC03OWE5LTRkMTUtOThiNS03YjUxZGJiMmNkMzEiLCJhdXRoZW50aWNhdGlvblR5cGUiOiJQQVNTV09SRCIsImVtYWlsIjoiYWRtaW5AZnVzaW9uYXV0aC5pbyIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiODVhMDM4NjctZGNjZi00ODgyLWFkZGUtMWE3OWFlZWM1MGRmIiwicm9sZXMiOlsiY2VvIl19

If you run the sample payload through a base64 decoder:

echo 'eyJhdWQiOiI4NWEwMzg2Ny1kY2NmLTQ4ODItYWRkZS0xYTc5YWVlYzUwZGYiLCJleHAiOjE2NDQ4ODQxODUsImlhdCI6MTY0NDg4MDU4NSwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDEiLCJqdGkiOiIzZGQ2NDM0ZC03OWE5LTRkMTUtOThiNS03YjUxZGJiMmNkMzEiLCJhdXRoZW50aWNhdGlvblR5cGUiOiJQQVNTV09SRCIsImVtYWlsIjoiYWRtaW5AZnVzaW9uYXV0aC5pbyIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiODVhMDM4NjctZGNjZi00ODgyLWFkZGUtMWE3OWFlZWM1MGRmIiwicm9sZXMiOlsiY2VvIl19' |base64 -d

You’ll see this JSON:

{
  "aud": "85a03867-dccf-4882-adde-1a79aeec50df",
  "exp": 1644884185,
  "iat": 1644880585,
  "iss": "acme.com",
  "sub": "00000000-0000-0000-0000-000000000001",
  "jti": "3dd6434d-79a9-4d15-98b5-7b51dbb2cd31",
  "authenticationType": "PASSWORD",
  "email": "admin@fusionauth.io",
  "email_verified": true,
  "applicationId": "85a03867-dccf-4882-adde-1a79aeec50df",
  "roles": [
    "ceo"
  ]
}

Note that the algorithm to create signed JWTs can remove base64 padding, so there may be missing = signs at the end of the JWT. You may need to add that back in order to decode a JWT token. This depends on the length of the content. You can learn more about it here.

As mentioned above, the payload is what your application cares about, so let’s take a look at this JSON more closely. Each of the keys of the object are called “claims”.

Some claims are well known with meanings dictated by standards bodies such as the IETF. You can view examples of such claims here. These include the iss and aud claims from the example token. Both of these have defined meanings when present in the payload of a JWT.

There are other non-standard claims, such as authenticationType. These claims may represent business domain or custom data. For example, authenticationType is a proprietary claim used by FusionAuth to indicate the method of authentication, such as password, refresh token or via a passwordless link.

You may add any claims you want to a JWT, including data useful to downstream consumers of the JWT. As you can see from the roles claim, claims don’t have to be simple JSON primitives. They can be any data structure which can be represented in JSON.

Claims to Verify

When code is presented with a JWT, it should verify certain claims. At a minimum, these claims should be checked out:

In addition to these, verify business domain specific claims. For instance, someone consuming the above JWT could deny access when authenticationType is an unknown value.

Avoid putting unused claims into a JWT. While there is no limit to the size of a JWT, in general the larger they are, the more CPU is required to sign and verify them and the more time it takes to transport them. Benchmark expected JWTs to have an understanding of the performance characteristics.

Claims and Security

The claims of a signed JWT are visible to anyone who possesses the token.

As you saw above, all you need to view the claims in plaintext is a base64 decoder, which is available at every command line and everywhere in the internet.

Therefore, you shouldn’t put anything that should remain secret into a JWT. This includes:

Another security concern is related to the verification of the aud claim. Since consuming code already possesses the token, isn’t verifying the aud claim extra work? The aud claim indicates who should receive this JWT, but the code already has it. Nope, always verify this claim.

Why?

Imagine a scenario where you have two different APIs. One is to create and manage todos and the other is a billing API, used to transfer money. Both APIs expect some users with a role of admin. However, that role means vastly different things in terms of what actions can be taken.

If both the todo and billing APIs don’t verify that any given JWT was created for them, an attacker could take a JWT from the todo API with the admin role and present it to the billing API.

This would be at best a bug and at worst an escalation of privilege with negative ramifications for bank accounts.

The JWT Signature

The signature of a JWT is critical, because it guarantees the integrity of the payload and the header. Verifying the signature must be the first step that any consumer of a JWT performs. If the signature doesn’t match, no further processing should take place.

While you can read the relevant portion of the specification to learn how the signature is generated, the high level overview is:

When the JWT is received, the same operations can be performed. If the generated signature is correct, the contents of the JWT are unchanged from when it was created.

JSON Web Token Limits

In the specifications, there are no hard limits on length of JSON Web Tokens. In practical terms, think about:

Storage

JWTs can be sent in HTTP headers, stored in cookies, and placed in form parameters. In these scenarios, the storage dictates the maximum JWT length.

For example, the typical storage limit for cookies in a browser is typically 4096 bytes, including the name. The limit on HTTP headers varies widely based on software components, but 8192 bytes seems to be a common value.

Consult the relevant specifications or other resources for limits in your particular use case, but rest assured that JWTs have no intrinsic size limits.

Performance Penalty

Since JWTs can contain many different kinds of user information, developers may be tempted to put too much in them. This can degrade performance, both in the signing and verification steps as well as in transport.

For an example of the former, here are the results of a benchmark from signing and verifying two different JWTs. Each operation was done 50,000 times.

This first JWT had a body approximately 180 characters in length; the total encoded token length was between 300 and 600, depending on the signing algorithm used.

hmac sign
  1.632396   0.011794   1.644190 (  1.656177)
hmac verify
  2.452983   0.015723   2.468706 (  2.487930)
rsa sign
 28.409793   0.117695  28.527488 ( 28.697615)
rsa verify
  3.086154   0.011869   3.098023 (  3.109780)
ecc sign
  4.248960   0.017153   4.266113 (  4.285231)
ecc verify
  7.057758   0.027116   7.084874 (  7.113594)

The next JWT payload was of approximately 1800 characters, so ten times the size of the previous token. This had a total token length of 2400 to 2700 characters.

hmac sign
  3.356960   0.018175   3.375135 (  3.389963)
hmac verify
  4.283810   0.018320   4.302130 (  4.321095)
rsa sign
 32.703723   0.172346  32.876069 ( 33.072665)
rsa verify
  5.300321   0.027455   5.327776 (  5.358079)
ecc sign
  6.557596   0.032239   6.589835 (  6.624320)
ecc verify
  9.184033   0.035617   9.219650 (  9.259225)

You can see that the total time increased for the longer JWT, but typically not linearly. The increase in time taken ranges from about 20% for RSA signing to approximately 100% for HMAC signing.

Be mindful of additional time taken to transport longer JWT; this can be tested and optimized in the same way you would with any other API or HTML content.

Conclusion

Signed JWTs have a header, body, and signature. Each plays a vital auth role in ensuring that JWTs can be used to safely store and transmit critical information, whether about identities or not. Understanding all three of these components are critical to the correct use of JWT as well.