What is a JSON Web Token (JWT)?
JSON Web Token (JWT) is widely used in modern web applications and open standards such as OpenID Connect, facilitating authentication and authorization. While the official RFC 7519 serves as an essential reference, it will be challenging for beginners to understand. In this article, we’ll focus on the core concepts of JWT and present them in straightforward language with examples.
Why do we need JWT?
Nowadays, it’s pretty common to use JSON to exchange data between two parties. Consider a JSON object representing a user:
{
"sub": "foo",
"name": "John Doe"
}
sub
is short for “subject”, which is a standard claim in OpenID Connect to represent the user identifier (user ID).
How can we guarantee the integrity of this JSON object? In other words, how can we make sure that the data is not tampered with during the transmission? A common solution is to use digital signatures. For example, we can use public-key cryptography : the server signs the JSON object with its private key, and the client can verify the signature with the server’s public key.
In a nutshell, JWT offers a standardized approach to representing the JSON object and its signature.
JWT can also be used to encrypt the JSON object, but it’s not the focus of this article.
The format of JWT
Since there are many algorithms for creating digital signatures, it’s necessary to specify the algorithm used for JWT signing. This is accomplished by constructing a JSON object:
{
"alg": "HS256",
"typ": "JWT"
}
alg
is short for “algorithm”, andtyp
is short for “type”.
Typically, typ
is set to JWT
in uppercase. For our example, alg
is HS256
, which stands for
HMAC-SHA256 (we’ll explain it soon), and indicates we’re using this algorithm to create the signature.
Now, we have all the ingredients for a JWT:
- Header JSON: Algorithm and type
- Payload JSON: The actual data
- Signature: The signature encompassing the header and payload
However, certain characters like spaces and line breaks are not friendly to network transmission. Therefore, the header and payload need to be Base64URL-encoded. The typical JWT looks like this:
{{header}}.{{payload}}.{{signature}}
The
.
serves as the delimiter.
Let’s put everything together and create a JWT:
Header
JSON: {"alg":"HS256","typ":"JWT"}
Base64URL encoded: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload
JSON: {"sub":"foo","name":"John Doe"}
Base64URL encoded: eyJzdWIiOiJmb28iLCJuYW1lIjoiSm9obiBEb2UifQ
Signature
In HMAC-SHA256, the signature is created with a secret:
HMAC-SHA256(base64Url(header) + "." + base64Url(payload), secret)
For instance, with the secret as some-great-secret
, the signature becomes: XM-XSs2Lmp76IcTQ7tVdFcZzN4W_WcoKMNANp925Q9g
.
JWT
The final JWT is:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28iLCJuYW1lIjoiSm9obiBEb2UifQ.XM-XSs2Lmp76IcTQ7tVdFcZzN4W_WcoKMNANp925Q9g
This valid JWT can be verified by any party possessing the secret.
Choose the signing algorithm
As mentioned earlier, there are various algorithms to create digital signatures. We used HS256
as an example, but it may not be strong enough since the secret must be shared between the parties (e.g. the client and the server).
In real-world scenarios, clients may include public applications like React apps that cannot keep the secret safe. Consequently, the preferred approach involves utilizing public-key cryptography (i.e. asymmetric cryptography) for signing the JWT. Let’s start with the most popular algorithm: RSA .
RSA
RSA, an asymmetric algorithm, uses a pair of keys: a public key and a private key. The public key is used to verify the signature, while the private key is used for signing.
The header JSON for RSA looks like this:
{
"alg": "RS256",
"typ": "JWT"
}
RS256
stands for RSA-SHA256, which means the signature is created with the RSA algorithm and SHA256 hash function. You can also useRS384
andRS512
to create signatures with SHA384 and SHA512 hash functions, respectively.
The signature is created with the private key:
RSA-SHA256(base64Url(header) + "." + base64Url(payload), privateKey)
Again, we can assemble these parts to create a JWT, and the final JWT looks like this:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28iLCJuYW1lIjoiSm9obiBEb2UifQ.{{signature}}
Now, the client can verify the signature without knowing the private key.
ECDSA
Although RSA is widely adopted, it suffers from larger signature sizes, sometimes surpassing the combined size of the header and payload. The Elliptic Curve Digital Signature Algorithm (ECDSA) is another asymmetric algorithm that can create more compact signatures and is more performant.
To generate a private key for ECDSA, we need to choose a curve. This is out of the scope of this article, but you can find more information here .
The header JSON for ECDSA looks like this:
{
"alg": "ES256",
"typ": "JWT"
}
ES256
stands for ECDSA-SHA256, which means the signature is created with the ECDSA algorithm and SHA256 hash function. You can also useES384
andES512
to create signatures with SHA384 and SHA512 hash functions, respectively.
The signature is created with the private key:
ECDSA-SHA256(base64Url(header) + "." + base64Url(payload), privateKey)
The final JWT retains the same structure as RSA but with a significantly shorter signature:
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28iLCJuYW1lIjoiSm9obiBEb2UifQ.{{signature}}
Verify the JWT
Verifying a JWT is straightforward as the reverse process of creating a JWT:
- Split the JWT into three parts (header, payload, and signature) using the
.
delimiter. - Decode the header and payload with Base64URL.
- Verify the signature with the algorithm specified in the header and the public key (for asymmetric algorithms).
There are many libraries that are available to assist with JWT verification, such as jose for Node.js and web browsers.