As developers, we've all faced the headache of securing our applications. Whenever this topic enters the room, so do JSON Web tokens (JWT). This guide is my deep dive into JWTs, crafted from one developer to another. We'll explore the central role of JWTs in today's web development, how they work, and how to use them effectively. By the end, you'll be equipped with the knowledge to implement JWTs in your projects confidently.
What We'll Cover
With that being said, let’s dive into JWTs!
JWTs or JSON Web Tokens are a simple and secure way to transfer information. They are built around cryptography where the data is signed upon creating the token and therefore its content can be trusted by all other parties. When these tokens are signed they turn into a JSON Web Signature (JWS).
Actually, when everyone is talking about JWTs, they are in fact describing JWSs. Because without the signature the tokens couldn’t be used in a trusted and secure way as we’re going to describe further down below. But for consistency and aligning this message with the term the industry has set with we’re also going to refer to them as just JWTs.
JWTs is primarily used for authorization, where a token has been loaded with claims and given to the user after a login process. These claims are essentially pieces of information encoded within the token. Think of claims as attributes or properties about the user and their permissions. For example, a claim might state the user's ID, their role (like 'admin' or 'user'), and the token's expiration time. The user can then use this token to prove its identity and access rights every time a request to an API is made.
The challenge of maintaining user authentication and authorization has been essential to web development from its early days, leading developers to explore various solutions throughout the years. JWT came quite late to the party but after being introduced as an open standard (RFC 7519) in 2010 it gained widespread popularity by offering a stateless approach and serving as the core of widely used authentication protocols like OAuth and OpenID Connect (OIDC).
Before JWT, session-based authentication was the standard. Session-based authentication requires database lookups for each request, which is resource intensive and adds latency. As the internet expanded, applications served more users and microservices and third party API integrations became a thing. All the increased demand required a better solution.
JWT is a stateless solution that encapsulates all the authorisation information in a self-contained token that can be verified and trusted, without the need to keep states in the database.
JWTs are essentially just key-value pairs JSON and therefore they offer a more lightweight and readable format compared to other alternatives like the XML-based Security Assertion Markup Language (SAML).
They are also highly customizable. You add any information, or claims as they are called, to your tokens and customize expiration time, signing algorithms etc. The resulting token data is base64 encoded meaning it can easily be transferred over HTTP or any protocol without the risk of special characters breaking the message.
JWTs are issued and verified. When issued, a server generates a new JWT usually after a login process. This step involves adding all the user information as JWT claims and signing the data with a private key. The user client then stores this token and attaches it to every future API request.
There’s no rule in how tokens should be attached to these requests, but commonly, JWTs are sent as Bearer tokens in the HTTP Authorization header, a method popularized by the OAuth 2.0 specification (RFC 6750). This involves including the JWT in the Authorization header of HTTP requests with the prefix 'Bearer'. The term 'Bearer' indicates that the possessor of the token is granted access to the protected resource.
The server that recieves this request can validate the token signature with the public key and then unpack its content and authorize the call. As you understand the public key is central here and therefore servers need to access this key somehow.
In this figure the user wants to retrieve a protected resource from the “Resource Server”. After logging in and obtaining a JWT from the “Authentication Server” the user can provide this token to the “Resource Server” which will grab the public key from the “Authentication Server” and then render the protected resource. We’ll cover the different ways public keys can be distributed later below.
The JWT get’s loaded with claims which are essentially just key-value pairs. This is how information about the user is stored. There are both registered claims and private claims.
Registered claims, predefined by the JWT standard itself, are meant to establish the mechanics of the tokens and tell verifying servers what to verify.
Here’s a list of the registered claims of JWTs and what they represent.
Private claims is your way to define any additional information you want your tokens to carry, as long as you don’t collide with the registered ones, e.g. name=John and role=admin.
With all these claims added to the token we can provide enough information for a verifying server to fulfill its authorization process. The server will understand who signed the token, if the token is intended for this server, the id of the user and its role and when this access expires. Pretty powerful right?
Diving a few notch deeper into the anatomy JWTs a complete and signed token will look something like this:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3d3cuaXNzdWluZ3NlcnZlci5jb20iLCJpYXQiOjE3MTAyNjA4MjgsImV4cCI6MTc0MTc5NjgyOCwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiam9obkBkb2UuY29tIiwibmFtZSI6IkpvaG4iLCJyb2xlIjoiQWRtaW4ifQ.c4gnA9XSBJ1iGbCbeh8khIy0pnw8PR5MVSCzWoqG-qM
The token consists of three parts that are separated by a dot. Each part is encoded with Base64.
The decoded and unpacked data from the above JWT looks like this.
Header
This header tells us that this is a JWT (obviously) and that it was signed with a particular key with id 1234 using the HS256 algorithm. This is important because it tells the verifier what public key to pick from a key store, how to use it to verify the signature and ultimately trusting the data it contains. We’ll cover more on key stores and how to distribute public keys in the next section.
{ "alg": "HS256", "typ": "JWT", "kid": "1234" }
Payload
The payload carries all the claims and information we’ve added to the token.
{
"iss": "www.issuingserver.com",
"iat": 1710260828,
"exp": 1741796828,
"aud": "www.example.com",
"sub": "john@doe.com",
"name": "John",
"role": "Admin"
}
Signature
The last part is the signature. This data is just a binary string and it gives no valuable insight to dissect it.
How can you and others trust your users JWT you might wonder. Let’s recap the essentials of JWTs. Every server that wants to verify a token needs access to the public key from the key pair that was used to issue and sign it.
If the same server that issued it also is the one verifying it, access to the public key is probably not an issue. However, this quickly becomes hard to manage for applications that consist of a lot of different servers e.g. for microservice architectures or when using third party integrations. All those servers would need access to this same key. So how can you distribute them in a smart way?
Introducing JWKS
To solve this problem we can turn our heads to JWKS (JSON Web Key Set), this is a format to store public keys in JSON format and is part of the JSON Web Key (JWK) open standard RFC 7517. It has gained a lot of popularity with JWT plugins because it’s a great way to pack and transport public keys.
Within this standard it is declared how to set up an endpoint that exposes a JWKS where
servers can then obtain this with a simple HTTP GET request. Each public key in this set is tagged with a unique id named kid. When a server verifies a token it can then extract the key id from the JWT header and pull the right key from the JWKS endpoint. In your implementation you can use JWT plugins such as jose or jsonwebtoken together with jwks-rsa to name a few in the NPM world.
The JWKS HTTP endpoint is recommended to be exposed on the URL https://your-signing-server.com/.well-known/jwks.json on the issuing server. Here’s an example output from our JWKS endpoint https://auth.nblocks.cloud/.well-known/jwks.json.
{
"keys": [
{
"kty": "RSA",
"n": "vgfhAWrj-e8VlV_alfgtZtZpQu2tEdU-_Goi5BN23p",
"e": "AQAB",
"kid": "ovaqkxAHX5ve8VlV_alf",
"use": "sig"
},
{
"kty": "RSA",
"n": "_Goi5BN23p-e8VlV_alfgtZtZpQu2tEdU-vgfhAWrj",
"e": "AQAB",
"kid": "e8VlV_alfgtZtZpQu2tEdU",
"use": "sig"
}
]
}
When deploying a JWT solution it is recommended to periodically change the private and public key to mitigate risks with the private key getting in the hands of attackers. This is called key rotation. JWKS simplifies this process when you need to rotate the key pairs because this lets you hold both the old public key and the new one which makes it possible for existing tokens to still exist until they expire.
While the technology is relatively straightforward, simple mistakes can introduce serious vulnerabilities. To help you implement JWT authentication securely, we’ll cover some essential best practices and highlight common pitfalls to avoid.
Why It’s Important: Data transmitted over HTTP is unencrypted, making it susceptible to interception. Attackers could steal tokens and gain unauthorized access to user data. HTTPS encrypts data in transit, protecting against eavesdropping and ensuring that tokens are securely exchanged.
Example: Consider a login request sent over HTTP. An attacker could easily intercept this and obtain the JWT token, allowing them to impersonate the user. Using HTTPS prevents this by encrypting the data between the user’s browser and the server.
HTTPS is also important from an identity perspective. As we described above when setting up a JWKS endpoint it is essential that other servers can rest assured they are picking the public key from the expected source.
The Pitfall: Failing to validate the JWT signature allows attackers to modify the token, potentially gaining unauthorized access. Signature validation ensures the token hasn't been tampered with. Many plugins ship with a decode function, for debugging purposes, that bypasses the signature verification process so watch out for this easy mistake.
Example: Imagine receiving a JWT token that claims the user has admin rights. Without signature validation, there’s no way to confirm if the token was indeed issued by your server or has been tampered with by an attacker.
The Risk: The JWT payload is encoded but not encrypted, making it readable by anyone who intercepts the token. Avoid storing confidential information in the token.
Solution: If you must transmit sensitive information, consider using JSON Web Encryption (JWE) for full encryption.
The Issue: Improperly storing JWTs, especially on the client side, can expose them to theft, e.g., through Cross-Site Scripting (XSS) attacks.
Best Practice: Treat tokens like credentials and store them securely and consider HTTP-only cookies or secure, encrypted storage solutions to prevent unauthorized access.
The Danger: Weak or compromised signing algorithms can allow attackers to forge tokens.
Recommendation: Use robust, secure algorithms like RS256 for signing JWTs to ensure their integrity and authenticity.
Common Mistake: Not setting an expiration for the token, or setting it too far in the future, leaves the token vulnerable to replay attacks if an attacker were to get access to the token and use it to impersonate the user.
Best Practice: Choose the shortest practical expiration time for tokens to minimize the window of opportunity for an attacker. But wouldn't short lived tokens result in real users getting logged out? In OAuth 2.0 (RFC 6749) this is handled with refresh tokens, a token that lives for a longer period and is used to generate new short lived access tokens.
Why It Matters: Regularly updating the cryptographic keys used to sign JWTs helps mitigate the risk of key exposure and token forgery.
Strategy: Rotate keys bi-monthly and ensure that old keys are retired securely to prevent misuse.
Purpose: These claims verify that the token is being used by the intended party and prevent the token from being misused in different contexts.
Example: An issuer claim can verify that the token was issued by your server, while an audience claim ensures it’s used by the intended recipient or service.
Consideration: Tokens embedded in HTTP headers have size limitations. Some servers don't accept more than 8 KB in headers and excessive token size can lead to issues.
Solution: Keep tokens concise and avoid embedding unnecessary data to ensure compatibility with servers and intermediaries.
We've now deep dived into the world of JSON Web Tokens (JWTs), unpacking their structure, significance, and how they integrate into modern web development for secure authentication and information exchange. From understanding the basics, diving into the mechanics, to best practices and common pitfalls, this guide aims to arm you with the knowledge you need to implement JWTs with confidence.
With the insights and practical advice shared, you're now well-equipped to navigate the complexities of JWTs in your projects. Whether it's for securing APIs, implementing single sign-on, or any other scenario where secure information exchange is paramount, you can move forward with the assurance that you're up to the task.
As we wrap up, remember that mastering JWTs, like any other technology, comes with continuous learning and experimentation. The resources provided towards the end offer further avenues to explore and deepen your understanding.
Thank you for joining me and armed with this knowledge, I'm confident you're well on your way to enhancing your applications' security and efficiency. Until next time, happy coding!