After a few years working as a backend developer i have come across JSON Web Tokens quite a few times. I have used them to secure my applications, but i never really understood how they work. So i decided to take a look into the JWT RFCs that define the standard and share what i have learned.
These will be our topics:
- What are JSON Web Tokens?
- The structure of a JWT
- JSON Web Signature (JWS)
- JSON Web Encryption (JWE)
- JSON Web Algorithms (JWA)
- JSON Web Key (JWK) and JSON Web Key Set (JWKS)
- When to use JWTs?
What are JSON Web Tokens?
So let’s start with the basics. A JSON Web Token is a compact, URL-safe way to represent claims that are securely transmitted between two parties. If that sounds like a lot of jargon all at once, don’t worry. Let’s understand what this means:
- It’s compact: The entire token is usually just a string that’s easy to include in the header of an HTTP request or as a query parameter. It doesn’t get too large and is often used in a way that it can fit neatly into an HTTP Authorization header, for example.
- It’s URL-safe: Meaning that you can take the string and place it in a URL without messing up how the URL is interpreted by web browsers or servers. This is important if you ever need to redirect with the token included in a link.
- It’s used to represent claims: A “claim” is simply a statement about an entity. In authentication contexts, a claim might be something like: “This user’s name is Alice” or “This user has the role of ‘admin’.” But it can be any piece of information you want to store and share with a recipient.
- It’s securely transmitted between two parties: While you can choose to encrypt a token’s content (more on that later), the main security measure in a basic JWT is the signature, which allows the receiver to verify that the content hasn’t been tampered with. Even without encryption, this signature is crucial: it ensures the data is authentic, though not necessarily secret.
Conceptually, you can think of a JWT like a sealed envelope containing some statements about you, such as your user ID, your roles, or any other details. The sealing process is the signature. When someone receives that envelope, if the seal is intact (meaning the signature is valid), they trust that the claims inside the envelope are the same ones the original issuer placed there. If you encrypt the content, it’s like sealing the envelope in an opaque wrapper so that not only can the envelope not be tampered with, but it also can’t be read by someone who intercepts it.
Why do I need JWTs specifically?
Well, JSON Web Tokens are widely recognized standards backed by the IETF (Internet Engineering Task Force). They’re not tied to any specific programming language or technology stack, so they play well with almost every modern framework. They also integrate nicely into existing workflows because they can be passed as HTTP headers, and they generally reduce the overhead involved in server-side session management.
The Structure of a JWT
Even though JWTs can look like a random string of characters, they’re actually structured in a very predictable way. If you’ve ever seen one in an app, you might recognize that it typically has three parts, separated by dots:
<Header>.<Payload>.<Signature>
Each of those parts is a Base64URL-encoded piece of JSON. When you decode it, you’ll find three main sections: a Header, a Payload, and a Signature. Let’s talk about each of these in more detail.
JWT Header
The Header is usually quite small and typically consists of information about the type of token and the content of that is stored in the payload. The JWT standard defines two fields that can be present in the header: typ and cty.
Field | Required | Description |
---|---|---|
typ | Yes | The type of the token. This can be used to help applications determine the type of token and how to parse and process them. (Ex: JWT, JWS, JWE) |
cty | No | The type of the content that is stored in the payload. This is very useful when the payload is not a JSON object. For example, it can be another JWT or just plain text. |
An example of a JWT Header might look like this when decoded from Base64URL:
{
"typ": "JWT",
"cty": "application/json"
}
Once you’ve written this JSON, it’s then Base64URL-encoded so it can be stuffed into the overall JWT string.
JWT Payload
Here is where the information that you want to transfer will be. The Payload contains the claims or statements about the entity (like the user) that the token is describing.
For example, maybe your app wants to say that the user’s name is “Alice,” or that the user’s ID is 123
.
You could put that information here.
These claims are also in JSON, so you might see something like this:
{
"sub": "1234567890",
"name": "Alice",
"admin": true,
"iat": 1516239022
}
When it comes to the payload, people often talk about registered claims and custom claims. Let’s understand what they are:
Registered Claims
Registered claims are predefined fields that are part of the JWT specification. They’re not mandatory to include in every JWT, but they are recognized as standard.
Claim | Description |
---|---|
iss (Issuer) | The issuer of the token |
sub (Subject) | The subject of the token (commonly the user ID) |
aud (Audience) | The audience of the token |
exp (Expiration Time) | The expiration time of the token |
nbf (Not Before) | The time before which the token must not be accepted for processing |
iat (Issued At) | The time at which the token was issued |
jti (JWT ID) | A unique identifier for the token |
These are primarily used for standardizing how we define things like an expiration or an intended audience. If you see these in a token, you know exactly what they mean because they’ve been documented in the specification.
{
"iss": "https://example.com",
"sub": "1234567890",
"aud": "example.com",
"exp": 1516239022,
"nbf": 1516239022,
"iat": 1516239022,
"jti": "1234567890"
}
Custom Claims
Custom claims are any additional pieces of data that your application wants to include.
For instance, you might want to store the user’s role ("role": "admin"
) or a permission set ("permissions": ["read", "write"]
).
As long as you don’t conflict with the registered claims, you’re free to add whatever fields you like.
If you’re building an app, you’ll typically want to be cautious not to bloat the payload with too much info. Remember that the JWT is sent every time the user makes a request (if you’re using it for stateless authentication). The bigger your token, the more data you’re pushing over the wire each time. So, keep it as lean as possible while still including what your app needs.
{
"role": "admin",
"email": "[email protected]"
}
And the JWT Signature?
The signature of a JWT is optional, but its use is recommended. The JWT standard defines the existence of this block, but not its implementation. From here we need to make one thing clear: JWT is just a base for building more sophisticated token types. It is very rare to find an application that uses a pure JWT, with only the header and payload, as this type of implementation is considered very insecure. For a secure implementation of JWT, we must use encryption or a digital signature.
The specifications on how to encrypt or digitally sign a JWT is known as Javascript Object Signing & Encryption (JOSE). This specification is composed by the following elements:
Acronym | Name | Description |
---|---|---|
JWS | Json Web Signature | Defines the process of digitally signing a JWT |
JWE | Json Web Encryption | Defines the process of encrypting a JWT |
JWK | Json Web Key | Defines the JSON structure that represents a cryptographic key |
JWA | Json Web Algorithm | Defines the valid algorithms for digitally signing or encrypting a JWT |
With these concepts clarified, we can finally take a look at how a JWT is signed.
JSON Web Signature (JWS)
A JSON Web Signature (JWS) is a mechanism for securing data by creating a digital signature of a JSON-structured payload. While JWT is often used in combination with JWS, JWS can be used on its own whenever you need to transmit JSON data with an attached proof of authenticity. JWT specifically uses JWS to sign its header and payload, but JWS can be more general.
In the context of JWT, whenever you’re signing the token, you’re basically making a JWS. The JWS specification outlines how to take a header, a payload, and a chosen cryptographic algorithm, and then produce a signature.
JWS is the name of the specification that defines how to digitally sign a JWT. But is also the name of all tokens that are signed using this specification.
The JWS specification defines the same three parts that a JWT has: a header, a payload, and a signature.
<Header>.<Payload>.<Signature>
JWS Header
The header of a JWS token can contain the same base attributes that a JWT header can have, but it extends it to include a set of attributes that are specific to the JWS specification. They are:
Attribute | Description |
---|---|
alg (Algorithm) | The algorithm used to sign the token, it is the only required attribute |
kid (Key ID) | Identifies the specific key used to sign the token, useful in systems with multiple public keys as in an authorization server |
x5u (X.509 URL) | A URL that points to an X.509 certificate associated with the public key that can be used to validate the signature |
x5c (X.509 Certificate Chain) | Contains the X.509 certificate chain used to validate the signature |
x5t (X.509 Certificate SHA-1 Thumbprint) | A SHA-1 hash of the X.509 certificate associated with the public key |
x5t#S256 (X.509 Certificate SHA-256 Thumbprint) | Similar to x5t, but uses SHA-256 to calculate the hash |
jku (JWK Set URL) | A URL that points to a set of public keys in JWK (JSON Web Key) format that can be used to verify the signature |
jwk (JSON Web Key) | Contains the public key directly in the header, in JWK format, for signature validation |
The most basic JWS header looks like this:
{
"alg": "HS256",
"typ": "JWT",
"cty": "application/json"
}
But if you are handling a more complex scenario, you might need to include more attributes:
{
"alg": "HS256",
"kid": "key-1",
"x5u": "https://example.com/cert.pem",
"x5c": ["MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwq3z1Q=="],
"x5t": "d2b1b3f1",
"x5t#S256": "d2b1b3f1",
"jku": "https://example.com/jwks.json",
"jwk": {
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmb8E",
"e": "AQAB"
}
}
But in a pure JWS context, typ
might not necessarily be "JWT"
. Instead, it might indicate the content type or might be omitted.
The critical part is "alg"
, which tells us which algorithm to use (e.g., HS256
for HMAC with SHA-256, RS256
for RSA with SHA-256, and so on).
JWS Payload
The payload of a JWS token is the same as the payload of a JWT token. It contains the claims that are being transmitted between the parties.
JWS Signature
The last part of a JWS. You take the base64Url encoded header and payload, join them with a .
and then apply your chosen algorithm along with a secret key or private key, depending on which algorithm you’re using.
The output is a digital signature. This signature is appended as the last piece of the JWS. In JWT form, that results in the familiar structure (the linebreak is for readability purposes):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The signing algorithm in a JWS can be symmetric or asymmetric.
In symmetric algorithms (like HS256
), you use a single shared secret to create and validate the signature.
In asymmetric algorithms (like RS256
), you use a private key to create the signature and a corresponding public key to validate it.
Each approach has its own tradeoffs and security implications, but the main point is that once the JWS is formed, the signature provides integrity and authenticity guarantees.
JSON Web Encryption (JWE)
So far, we’ve talked about signatures and how they ensure that a token hasn’t been tampered with. However, if you’re sending along sensitive information that you don’t want prying eyes to see, you need something stronger: encryption. This is where JSON Web Encryption (JWE) comes in. JWE deals with how to securely encrypt JSON data, so that only the intended recipient can read the contents.
When you create a JWE, you’re essentially turning your data into ciphertext that can only be deciphered by someone with the correct key.
JWE is the name of the specification that defines how to encrypt a JWT. But is also the name of all tokens that are encrypted using this specification.
The JWE specification defines a structure that is a little bit different from the JWS and JWT. It is composed by five parts: a header, a encrypted key, an initialization vector, the ciphertext and the authentication tag.
<Header>.<EncryptedKey>.<IV>.<Ciphertext>.<AuthenticationTag>
JWE Header
The JWE Header is similar in concept to the JWS Header but focuses on encryption details. You might see fields like:
Attribute | Description |
---|---|
alg (Algorithm) | The algorithm used to encrypt the content encryption key (CEK). This algorithm protects the key used to encrypt the payload |
kid (Key ID) | Identifies the specific key used to encrypt the CEK. Useful in systems with multiple encryption keys |
x5u (X.509 URL) | A URL that points to an X.509 certificate associated with the public key that can be used to decrypt the token |
x5c (X.509 Certificate Chain) | Contains the X.509 certificate chain used to decrypt the token |
x5t (X.509 Certificate SHA-1 Thumbprint) | A SHA-1 hash of the X.509 certificate associated with the public key |
x5t#S256 (X.509 Certificate SHA-256 Thumbprint) | Similar to x5t, but uses SHA-256 to calculate the hash |
jku (JWK Set URL) | A URL that points to a set of public keys in JWK format that can be used to decrypt the token |
jwk (JSON Web Key) | Contains the public key directly in the header, in JWK format, for decryption |
enc (Encryption Algorithm) | Defines the symmetric encryption algorithm used to encrypt the payload. This algorithm protects the data |
apu (Agreement PartyU Info) | Additional information specific to the party that initiates the process (PartyU). Used in key exchange schemes |
apv (Agreement PartyV Info) | Additional information specific to the receiving party (PartyV). Used in key exchange schemes |
zip (Compression Algorithm) | Defines the compression algorithm used on the payload before encryption |
From all these attributes, only the “alg” and “enc” are required. The others are optional and can be used to provide additional information about the encryption process.
Here is an example of a JWE header:
{
"alg": "RSA-OAEP-256",
"kid": "key-1",
"x5u": "https://example.com/cert.pem",
"x5c": ["MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwq3z1Q=="],
"x5t": "d2b1b3f1",
"x5t#S256": "d2b1b3f1",
"jku": "https://example.com/jwks.json",
"jwk": {
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmb8E",
"e": "AQAB"
},
"enc": "A256GCM",
"zip": "DEF"
}
Or a more typical and simple example:
{
"alg": "RSA-OAEP",
"enc": "A256GCM",
"typ": "JWT",
"kid": "key-id-12345"
}
This header is then Base64URL-encoded and becomes the first part of the JWE.
JWE Encrypted Key
Once you have your header, the next step in building a JWE is dealing with the encrypted key.
If you’re using a key management scheme, the actual key used to encrypt your content (the CEK) is encrypted using an algorithm specified by alg
.
For example, if alg
is RSA-OAEP
, your CEK will be RSA-OAEP-encrypted with the recipient’s public key.
This means only someone with the corresponding private key can decrypt that CEK.
The result of that encryption is placed in the “Encrypted Key” portion of the JWE. So in a JWE structure, after the header, you include the encrypted CEK, also Base64URL-encoded.
JWE Initialization Vector (IV)
Many encryption algorithms, especially block ciphers used in GCM or CBC mode, require an Initialization Vector (IV). The IV provides randomness so that if the same data is encrypted multiple times, it won’t yield the same ciphertext. This is crucial for security to prevent attackers from noticing patterns. The IV is usually randomly generated and must be supplied to the decryption process. In a JWE, the IV is also placed in its own Base64URL-encoded segment so that the recipient knows how to decrypt the ciphertext.
JWE Ciphertext
The ciphertext is, of course, your original message after being scrambled by the encryption process. If you’re using a JWT with JWE, this ciphertext includes the encrypted version of the JWT payload (and possibly other data, depending on how you structure it). This is the core piece that ensures confidentiality. Only someone with the correct key can turn this ciphertext back into the original plaintext.
JWE Authentication Tag
Finally, if you’re using an authenticated encryption scheme like Galois/Counter Mode (GCM), you’ll have an authentication tag that helps ensure the integrity of the encrypted message. This tag verifies that the ciphertext hasn’t been modified in transit. If an attacker tries to alter even a single bit of the ciphertext, the authentication tag validation should fail, and the decryption process won’t yield any meaningful plaintext.
Putting it all together, a JWE has multiple parts concatenated with dots (the linebreak is for readability purposes):
eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00iLCJraWQiOiIxMjM0NSJ9
.
ZFNBZ01kYzNvUTZQc1poM2FhcU1yQ2dFVnZoSlJlc2k
.
AxY8DCtDaGlsbGljb3RoZQ
.
4OjBYGr4zLEJlzI2ojT4F87xOnI
.
QK3Pj5Izj3vJDa2cSIRifg
Each of these parts is Base64URL-encoded. The result is that you can pass around a single string that includes everything needed for the recipient to decrypt the message (except for the recipient’s private key, of course).
At this point, you might be wondering how JWTs fit into all of this. A “JWE-based JWT” is essentially a JWT that has been encrypted using JWE. Instead of being a simple, signed token in the open, the payload is encrypted so that only the intended recipients can read it. You might still sign it as well, leading to what’s sometimes called a nested JWT — a JWT that’s both signed and encrypted.
Here is a diagram that shows the process of generating a JWE token:
JSON Web Algorithms (JWA)
Both JWS and JWE rely on a set of algorithms to handle signing and encryption. The specification that covers these is sometimes referred to as JSON Web Algorithms (JWA). This spec outlines which algorithms must be supported, which ones are recommended, and which are optional or deprecated.
(These are the algorithms that can be present in the “alg” header of a JWS or JWE token)
algoritmo | Descrição |
---|---|
HS256 | HMAC using SHA-256 |
HS384 | HMAC using SHA-384 |
HS512 | HMAC using SHA-512 |
RS256 | RSASSA-PKCS1-v1_5 using SHA-256 |
RS384 | RSASSA-PKCS1-v1_5 using SHA-384 |
RS512 | RSASSA-PKCS1-v1_5 using SHA-512 |
ES256 | ECDSA using P-256 and SHA-256 |
ES384 | ECDSA using P-384 and SHA-384 |
ES512 | ECDSA using P-521 and SHA-512 |
PS256 | RSASSA-PSS using SHA-256 and MGF1 with SHA-256 |
PS384 | RSASSA-PSS using SHA-384 and MGF1 with SHA-384 |
PS512 | RSASSA-PSS using SHA-512 and MGF1 with SHA-512 |
none | No digital signature or MAC performed |
For signing (JWS), common algorithms include:
- HS256: HMAC with SHA-256 (symmetric)
- RS256: RSA with SHA-256 (asymmetric)
- ES256: ECDSA with SHA-256 (asymmetric using elliptic curves)
For encryption (JWE), you typically see a combination:
- A key management algorithm like
RSA-OAEP
orECDH-ES
. - A content encryption algorithm like
A128GCM
,A192GCM
, orA256GCM
.
The reason we have separate algorithms for key management and content encryption is because in many advanced cryptographic schemes (like RSA or ECDH), you don’t directly encrypt large amounts of data with an asymmetric algorithm. Instead, you use the asymmetric algorithm to encrypt a symmetric key (the CEK). Then you use that CEK with a fast symmetric algorithm (like AES-GCM) to encrypt the actual payload. This approach is both efficient and secure.
When you look at a JWT, you’ll typically see something like "alg": "HS256"
in the header if it’s just signed.
If it’s encrypted, you’ll see something like "alg": "RSA-OAEP"
, "enc": "A256GCM"
, or similar.
Each combination indicates how the receiving party should attempt to decrypt or verify the token.
JSON Web Key (JWK) and JSON Web Key Set (JWKS)
If you’re building a system that uses JWTs, you’ll likely come across JSON Web Keys (JWK) and JSON Web Key Sets (JWKS). These specs define a standard way of representing cryptographic keys in JSON. Let’s say you have an RSA public key that you want to share with other services so they can verify tokens you’ve signed. You can provide them with your public key in JWK format.
A JSON Web Key looks something like this for an RSA public key:
{
"kty": "RSA", // Indicates the key type, e.g., “RSA” or “EC.”
"n": "oahUIo87v...4rUwhlHC8Q", // Modulus (Base64URL encoded)
"e": "AQAB", // Public exponent (Base64URL encoded)
"alg": "RS256", // Algorithm associated with the key
"kid": "12345", // The key ID, which helps you identify which key to use among many.
"use": "sig" // Use of the key (sig for signature, enc for encryption)
}
A JSON Web Key Set (JWKS) is simply a collection of these keys in JSON format. So instead of providing a single key, you provide an object like:
{
"keys": [
{
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmb8E",
"e": "AQAB",
"alg": "RS256",
"kid": "key-1"
},
{
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmb8E",
"e": "AQAB",
"alg": "RS256",
"kid": "key-2"
}
]
}
This approach makes it easy to rotate keys, or to publish multiple keys at once if you’re using them for different purposes. For instance, you might have one key for signing tokens and another for encrypting them. You can put both in the same JWKS. The advantage of having a JWKS endpoint is that client applications can automatically fetch your public keys to verify tokens without you having to share them manually each time.
When to use JWTs?
After all this detail, you might be asking: “Okay, so when is it appropriate to actually use JWTs in my application?” The answer depends on the design of your system and your security needs. However, here are some typical scenarios where JWTs shine:
JWS (Signed JWT)
When security is a concern, JWTs should be signed using JWS to ensure data integrity. Some practical applications include:
- Secure API Authentication: In RESTful or GraphQL APIs, JWS tokens provide tamper-proof identity verification. Even if intercepted, they cannot be altered without invalidating the signature.
- Identity Verification in OAuth 2.0 and OpenID Connect: ID tokens issued by an authentication provider (e.g., an OAuth authorization server) are signed JWS tokens, ensuring that the claims (such as user ID and roles) can be trusted by the relying party.
- Event and Log Integrity: Some systems use JWS to sign logs, events, or audit trails, ensuring that the data has not been modified after creation.
JWE (Encrypted JWT)
JWE is used when confidentiality is required in addition to integrity. Encrypted JWTs ensure that only intended recipients can read the token’s contents. Use cases include:
- Sensitive Data Transmission: In highly regulated industries (e.g., healthcare or finance), JWE can be used to transmit personally identifiable information (PII) securely between systems.
- Privacy-Focused Authentication Tokens: Unlike JWS, which keeps token claims readable, JWE encrypts the payload, preventing unauthorized disclosure of sensitive claims (e.g., roles, permissions).
- End-to-End Encryption in Messaging Systems: Some messaging applications use JWE to encrypt messages between users while still allowing metadata (such as sender and timestamp) to remain accessible.
Nevertheless, JWTs aren’t always the right tool for every scenario. For example, if you need to frequently invalidate user sessions, a server-based approach might be simpler. If you’re storing extremely large amounts of information in the token, you might cause performance issues. And if you’re worried about guaranteeing tokens are revoked, you might need a blacklist or short token lifespans, which complicates your design.
One last point about security: if you use JWTs, be sure to handle them carefully.
Always use HTTPS, so they’re not exposed over plain text.
Don’t store them in places vulnerable to cross-site scripting (like local storage in some contexts).
Also, pay attention to the alg
used in the header.
There have been vulnerabilities in the past where incorrectly configured libraries would let an attacker pass alg: none
.
Always ensure your server is strictly enforcing the algorithms you expect.