JWT: Don't Add Sensitive Information Into JSON WebToken (JWT)
We often use JWT for authentication without truly understanding its nature. We simply trust that it's secure. Now, it's time to dive deeper.
Think of JWT as a digital ID for online servers.
It’s like when you give your ID to a guard and they can quickly tell it’s real or not without calling someone to check. Similarly, with JWT, a server can confirm it’s valid using a key, without searching through a database each time.
1. What is JWT?
JSON Web Token (shortened to JWT) acts like a digital handshake between two participants, say you and a website. It’s a compact and secure method that ensures both parties can trust the exchanged information.
Here’s what a JWT looks like:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpYXQiOjE2OTg2ODg0OTksIm5hbWUiOiJEZXZ0cm92ZXJ0Iiwic3ViIjoidGhpcyBpcyBhIHN1YmplY3QifQ.
NT2JhQ9oLc_HIZB6WW8yAaxSHcZ8QvNE1H0C5Adhmrg
You’ll notice it’s split into 3 sections, each separated by dots (.).
The JWT acts as proof of who you are online.
Because it’s made in a special way, it’s tough for others to fake it. Plus, only websites that have the right “key” can understand it. This is great for times when you log into a website and use different parts of it without having to log in again and again.
“Why do we use JWT instead of just cookies or sessions?”
Remember the long JWT string above? When a server looks at it, the server knows right away who you are, what you’re allowed to do, and for how long and there’s no need for the server to save sessions or keep checking a database.
Everything it needs is in the JWT and this makes it easier to add more servers when needed.
Now, let’s break down the parts of the JWT.
2. Structure of JWT
A JWT technically consists of three parts separated by dots (.), which are:
a. Header
Starting with the first chunk before the dot, we have the Header. This is encoded using Base64 and reveals both the type of token and the algorithm behind the signature.
A quick look:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
=
{
"alg": "HS256", // the signature's algorithm
"typ": "JWT" // the token type
}
b. Payload
Moving to the middle, between those two dots, we encounter the Payload, this is also a Base64 encoded section.
Within the payload, you’ll find the “claims”, these are the tidbits of information, maybe the user’s ID or their email address.
Speaking of claims, they come in three flavors: registered, public, and private. Here’s a brief look:
eyJpYXQiOjE2OTg2ODg0OTksIm5hbWUiOiJEZXZ0cm92ZXJ0Iiwic3ViIjoidGhpcyBpcyBhIHN1YmplY3QifQ
=
{
"iat": 1698688499,
"name": "Devtrovert",
"sub": "this is a subject"
}
A word of caution: It’s best to avoid putting sensitive info in the payload, even though the JWT ensures the data hasn’t been changed, it doesn’t hide the content.
Now let’s explore those other claims further.
Registered
Let’s kick things off with registered claims.
They’re a bunch of predefined bits of information that the JWT standard provides for our benefit, while you don’t have to use them, it’s a good practice to do so:
iss (issuer): Think of this as the “author” of the JWT (e.g. “iss”: “auth.devtrovert.com”).
sub (subject): This is all about who the JWT is about, often pointing to a specific user (e.g. “sub”: “john-handsome”).
aud (audience): Who should be reading the JWT? It could be meant for one entity or many (e.g. “aud”: “example-app”).
exp (expiration time): Simply put, when does this JWT expire? It counts seconds since the Epoch time (e.g. “exp”: 1583241600).
nbf (not before time): It tells when we can start using the JWT, using a similar time format as exp. (e.g. “nbf”: 1583241600).
iat (issued at time): This is just when the JWT was made, like a timestamp of its creation (e.g. “iat”: 1583241600).
jti (JWT ID): A special ID for the JWT, which can be handy for various purposes like keeping track or ensuring it’s used only once.(e.g. “jti”: “a1234567”).
These are part of the JWT standard.
So, when you decide to use them, it’s wise to stick to the standard’s rules, even if they’re optional.
Public
Public claims are the ones you can set yourself.
But, because you don’t want to accidentally use the same claim name as someone else (or registered claims), it’s a neat idea to use a web address-like format.
Here’s what I mean:
"http://example.com/roles": ["editor", "subscriber"]
By using this structure, we make sure our “roles” claim is its own unique thing and doesn’t collide with any other “roles” claim out there.
Private
Private claims are claims that you make up to suit your app’s specific needs and they aren’t meant for the public, and there’s no official list of them anywhere.
“Hold up… If they’re called ‘private’, why can everyone read them?”
Good point, it’s a naming thing, despite the name, private claims aren’t secrets.
When we look inside a JWT, everything: both public and private claims, are out in the open because of how JWTs are encoded, so anyone with the right tools can see them.
The terms “public” and “private” in the context of JWT claims don’t refer to their visibility or their encryption status:
Public Claims: These are commonly understood claims, using a web URL format for naming helps keep things organized and avoids confusion.
Private Claims: These are your special additions, tailored just for your app and they get the “private” label because they’re all about your app’s unique needs
For instance, having a claim like “department”: “HR”
might mean the person associated with the JWT works in Human Resources.
c. Signature
The third part of our JWT, right after the last dot, we encounter the Signature.
What’s fascinating about this piece is that it’s constructed by joining the header and payload, then encrypting this merged data using a secret key. This signature ensures that our data remains untouched and authentic during its… journey.
For a hands-on perspective, imagine the process like this:
NT2JhQ9oLc_HIZB6WW8yAaxSHcZ8QvNE1H0C5Adhmrg
=
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
When pieced together, our JWT presents itself as:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpYXQiOjE2OTg2ODg0OTksIm5hbWUiOiJEZXZ0cm92ZXJ0Iiwic3ViIjoidGhpcyBpcyBhIHN1YmplY3QifQ.
NT2JhQ9oLc_HIZB6WW8yAaxSHcZ8QvNE1H0C5Adhmrg
Now, any server that verifies this token has the secret key, which is “mysecret” in this example. Unlike me, ensure you don’t share it with anyone.
This equips the server to decipher the signature and confirm the authenticity of the header and payload
A Dip into Algorithms
As we’re on the topic of cryptographic algorithms, it might be a good moment to share some basics about two algorithm types:
Symmetric: A single key is used for both operations. For encryption, it means both encryption and decryption, or both creating a signature and verifying it. Both parties (sender and receiver) must possess and protect this secret key. In the context of JWTs, typically only the server retains this secret key.
Asymmetric: This involves a key pair: a public key and a private key. For encryption, the public key encrypts and its corresponding private key decrypts. For digital signatures, the private key is used to sign (create a signature), and the public key is used to verify that signature.
Now, there are 3 commons algorithms used to sign and verify the JWTs:
HMAC (Symmetric): Uses a single secret key to create and check a signature for data, only server holds this key to confirm the data hasn’t been changed in transit. For JWTs, HMAC combined with SHA-256 hashing is common, known as HS256.
RSA (Asymmetric): Works with two keys: a public key and a private key. While you might hear about encrypting data with the public key and decrypting it with the private one, for JWTs, it’s a bit different. Here, the private key signs the token, and the public key checks its authenticity.
ECDSA (Asymmetric): This is a newer method for making digital signatures. While RSA is widely used, ECDSA can offer the same safety but is faster and needs shorter keys, this makes it a good choice for systems where speed and saving space are important.
3. How JWTs Are Used in Authentication
Let’s break down how JWTs play a vital role in the authentication process step by step:
Logging In: A user enters their username and password, the server checks these details against its records.
JWT creation: If the server confirms the user’s details are correct, it creates a JWT. This token contains the user’s details and other important information (but not sensitive). Then, the server signs this token with a secret key, ensuring its security.
Sending the Token: The server sends the token back to the user, this usually happens in the HTTP header for easy retrieval and usage in subsequent requests.
Token in Action: Whenever the user wants to access something on the server, they send this token back. They typically add it to their request headers, like a special access pass.
Server verification: When the server receives a request with a token, it verifies the token’s authenticity. It checks if the signature matches and then gets the user’s details from the token to process the request.
Response to the client: After validating the token and processing the request, the server sends the needed data back to the user.
One of the great things about JWTs is that they allow stateless authentication. What does this mean?
Well, the server doesn’t need to remember or store every token. Each token is self-contained, carrying all the info the server needs to identify the user.
4. How the JWT Token is Checked
When the user sends a token to the server, how does the server know it’s valid? Let’s go through that process:
Validate the Token Structure: Our server looks at the token to see if it has the right format, it should have three parts: the Header, the Payload and the Signature (structured like ‘xxxxx.yyyyy.zzzzz’).
Decoding: Our server then decodes the token, since it’s usually in Base64URL format, decoding it lets the server see the Header and Payload’s details.
Figuring Out the Algorithm: By checking the token’s header, the server can see which algorithm was used to sign the token.
Matching Signatures: The server then tries to recreate the token’s signature. If the new signature matches the one in the received token, it shows the token hasn’t been changed or tampered with.
Checking the Claims: Lastly, the server examines the JWT’s standard claims. It looks at timestamps and identifiers like when the token expires (‘exp’), when it starts being valid (‘nbf’), when it was made (‘iat’), who made it (‘iss’), who it’s for (‘aud’), and who it’s about (‘sub’).
If the token passes all these checks, the server knows it’s good to go.
A limitation of JWTs is, they can’t be revoked.
Once a JWT is created, it’s valid until it expires. So, if a JWT is stolen, it can be used by anyone until it expires, which is why it’s important to keep the expiration time short.
You can also use a blacklist to keep track of stolen or malformed tokens, but this means you’ll need to query a database/ cache.