# JWT: What It Is, Why It Exists, and Why You Shouldn't Build Your Own
A deep dive into JSON Web Tokens: what they are, why they were created, how libraries build them, and why rolling your own is a security risk.
JWT: What It Is, Why It Exists, and Why You Shouldn't Build Your Own
If you have built any web application in the last decade, you have probably used JWT. Maybe you followed a tutorial that included jsonwebtoken or jjwt and moved on.
This post covers what a JSON Web Token is, why it exists, how libraries actually produce that string of text, and why every security engineer tells you not to implement it yourself.
What JWT actually is
JWT stands for JSON Web Token. It is an open standard (RFC 7519) that defines a compact, URL-safe way to transmit claims between two parties.
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three parts separated by dots. Each part is Base64URL-encoded JSON. The header carries the algorithm, the payload carries the claims, and the signature is what stops tampering.
The header tells you what algorithm was used to sign the token:
{
"alg": "HS256",
"typ": "JWT"
}
The payload contains the claims about the user or the token itself:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
The signature is computed by taking the encoded header and payload, concatenating them with a dot, and signing that string using the algorithm specified in the header. It proves the token came from someone who knows the secret key (authenticity), and that nothing has been modified since signing (integrity).
Anyone can decode a JWT and read its contents. The payload is not encrypted, only encoded. The security comes from the signature, not from hiding the data. Do not put sensitive information like passwords in a JWT payload unless you are using JWE (JSON Web Encryption), which most people do not.
Why JWT was created
Before JWT, most web applications used session-based auth. A user logs in, the server stores a session in memory or a database, and hands back a session ID in a cookie. Every request requires the server to look up that session.
This works fine on one server. Add a second one behind a load balancer, and a user who authenticated on Server A gets routed to Server B, which has no record of them. Microservices compound the problem. Your Orders service needs to verify the user, so it calls Auth, which calls Redis. A network hop on every authenticated call.
A session lookup costs around 50ms. With thousands of concurrent users, that is hundreds of thousands of database calls per second just to check if someone is logged in.
JWT eliminates that overhead. The token is self-contained. Any server can validate it in about 5ms using only the secret key. No database call, no inter-service hop. The idea was simple: what if the token itself carried everything needed to verify it?
How JWT libraries create tokens
When you call jwt.sign(payload, secret) in a library, here is what happens.
First, the library builds the header. It creates a JSON object with the algorithm (alg) and token type (typ). If you specify { algorithm: 'HS256' }, the header becomes {"alg":"HS256","typ":"JWT"}. That JSON is Base64URL-encoded.
Next, it builds the payload. Your payload object is serialized to JSON and Base64URL-encoded. The library may inject standard claims like iat (issued at) or exp (expiration) depending on your options.
Then it creates the signature input by concatenating the encoded header and payload with a dot:
base64UrlHeader.base64UrlPayload
The library feeds this string through the cryptographic algorithm specified in the header. For HMAC-SHA256 (HS256), it uses the secret key you provided and produces a hash. For RSA-SHA256 (RS256), it uses your private key. For ECDSA, it uses your EC private key. The output is a fixed-size byte array.
Finally, the raw signature bytes are Base64URL-encoded and appended to the string with another dot:
base64UrlHeader.base64UrlPayload.base64UrlSignature
That is your JWT.
Verification works in reverse. The library splits the incoming token on the dots, re-computes the signature using the same algorithm and the public key (or secret), and compares the result to the signature on the token. If they match, the token is valid.
I looked at the source of several popular JWT libraries while researching this. The signing function in JJWT (Java) is about 30 lines. The sign function in node-jsonwebtoken is similarly compact. The complexity is not in the core signing flow. It sits in everything around it: key validation, algorithm whitelisting, claim verification, error handling, and the dozens of edge cases that can turn a minor mistake into a critical vulnerability.
Why you should not roll your own JWT library
The signing flow above looks simple. A few lines of code, some base64 encoding, a cryptographic call. The JWT specification leaves a lot of room for interpretation, and that room is where security vulnerabilities live.
These are the ones that show up most often in custom implementations.
The none algorithm attack. JWT supports an algorithm called none for unsigned tokens. It exists for testing and for cases where the token is already protected by a transport layer like TLS. A naive implementation reads the algorithm from the token header and uses it for verification. An attacker changes alg from HS256 to none, removes the signature, and the library accepts the token as valid. The server should never trust the algorithm from the header. Enforce a whitelist of allowed algorithms instead.
Algorithm confusion. Say your server expects tokens signed with RS256 (RSA, asymmetric). The server has a public key for verification. An attacker creates a token signed with HS256 (HMAC, symmetric) using the RSA public key as the HMAC secret. If the library picks the algorithm from the header, it switches to HMAC mode, uses the public key as the secret, and the forged token passes verification. The RSA public key is, by definition, public. Anyone who knows it can forge tokens the server will accept. The fix is to bind the algorithm to the key, so each key works with one algorithm only. Libraries like jose and jjwt enforce this. Custom implementations often miss it.
Missing claim validation. A token might have a valid signature but contain expired claims, the wrong audience, or data intended for a different service. Libraries handle all of these by default. Custom implementations often skip them because each check feels optional, and tokens that never expire end up being accepted across every endpoint.
Timing attacks. Comparing cryptographic signatures requires a constant-time comparison function. Standard string comparison returns early when the first byte mismatches. An attacker can measure response time and guess the correct signature byte by byte. Production libraries use constant-time comparison. Custom code rarely does.
RFC 8725 (JWT Best Current Practices) lists over a dozen categories of known vulnerabilities, and even that is not exhaustive. The JWT ecosystem has been picked apart by security researchers for years. Production libraries like jose, jjwt, and @node-rs/jsonwebtoken have fixed these issues through years of community scrutiny. Writing a JWT library from scratch means rediscovering every one of these vulnerabilities in production, with real user data.