~/blog/unlocking-the-power-of-jwt-securely-transmitting-information-in-the-digital-age
JWT: How Signed Tokens Work and What They Prove
How a JSON Web Token is built, signed and verified, what the signature does and does not prove, and where JWT fits in a microservices setup.

Photo by Emile Perron on Unsplash
A JWT gets described as "a secure way to transmit information," which is true and skips the part that actually matters: secure how, and proving what. The token's signature proves the payload wasn't altered after whoever holds the signing key signed it. It proves nothing about whether the token should still be trusted right now, and that gap is where most JWT-related incidents live.
This covers the three parts that make up a token and what each one is for, what verifying a signature actually checks, a real vulnerability class, algorithm confusion, that exploits a gap between what a library assumes and what the header actually says, the revocation problem JWT doesn't solve by design, and when a stateless token is the wrong tool for a session.
The three parts, and the one job each has#
A JWT is three base64url-encoded segments joined with dots: a header, a payload, and a signature.
{
"alg": "HS256",
"typ": "JWT"
}The header names the signing algorithm. This is the part that matters for security, not just metadata, for reasons the next section covers.
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}The payload carries claims: who the subject is, when the token was issued, when it expires (exp),
and whatever else the issuer wants to assert. The signature is computed over the encoded header and
payload together, using the algorithm the header named and a key only the issuer holds:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)The three parts, concatenated with dots, are the token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cWhat verifying the signature actually proves#
Verification recomputes the signature over the header and payload using the key the verifier has, and checks it matches the signature on the token. That proves two things and no more: the payload hasn't been modified since signing, and whoever produced the signature held a key the verifier trusts. It proves nothing about whether the user's permissions have changed since issuance, whether the account has since been suspended, or whether the token has been revoked. Those are all facts about now; the signature is a fact about the moment of issuance.
Algorithm confusion: when the header lies usefully#
The header naming its own algorithm is a convenience that became a real vulnerability class. Some
early JWT libraries trusted the alg field in the token to decide how to verify it, rather than
having the caller specify the expected algorithm up front. An attacker who knows a service verifies
RS256 tokens with a public key can craft a token with "alg": "HS256" and sign it using that same
public key as if it were an HMAC secret, which is often guessable or published. A library that reads
alg from the token and blindly switches verification mode accepts it.
// BAD: the algorithm to verify with comes from the token itself. An attacker
// controls the token, which means they control which check runs against it.
$decoded = JWT::decode($token, $key, [$token->header['alg']]);
// GOOD: the caller states the one algorithm it expects, and the library
// rejects anything else before touching the key at all.
$decoded = JWT::decode($token, $key, ['RS256']);A related variant: a token with "alg": "none" and an empty signature, which some early parsers
accepted as valid because an empty signature trivially "matched" an empty check. Both bugs have the
same root cause: trusting the attacker-controlled header to pick the verification mode, rather than
having the verifier's own configuration be the only source of truth for which algorithm is
acceptable. Every maintained JWT library has fixed this for years; the risk now is almost entirely in
hand-rolled verification code that re-reads alg from the token out of habit.
The revocation problem#
A JWT is stateless by design: the server verifying it doesn't need a database lookup, which is the whole point for a microservices setup where ten services would otherwise all need to hit the same session store. That design choice has a direct cost: there is no server-side session to delete, so a token issued with a one-hour expiry is valid for the full hour no matter what happens to the account in the meantime. Revoking a stolen token before its natural expiry means either keeping a blocklist of revoked token IDs (which reintroduces the shared-state lookup JWT was meant to avoid, just for a smaller set of records) or accepting the exposure window and keeping expiry short enough that it's tolerable. Short-lived access tokens (minutes, not hours) paired with a refresh token that is checked against server-side state on every use is the common compromise: the frequently-used token stays stateless and fast, the rarely-used one carries the revocation check.
When JWT is the wrong tool#
For a monolith with one database and no cross-service calls, a server-side session with a simple session ID cookie is simpler, revocable instantly, and gives up nothing JWT was buying you, since there was no second service that needed to verify the token independently in the first place. JWT earns its complexity when multiple services need to verify a caller's identity without each one calling back to a central auth service on every request. Reaching for it because it's the standard answer to "how do I do auth," on a system where that specific problem doesn't exist, adds a revocation problem you didn't previously have in exchange for a stateless-verification benefit you don't currently need.
$ related posts
· 20 min
Securing Event-Sourced Financial Systems
Part 4 of an event sourcing series: PII kept out of events, crypto-shredding for GDPR erasure, locked-down CQRS projections, and auditable observability.
20 min
· 9 min
Event Store Bottleneck: Scaling with NestJS Microservices
Your event store processes 10K events/min smoothly, then Black Friday hits. How to scale with distributed NestJS microservices.
9 min
· 8 min
Encryption, Encoding and Hashing Are Not the Same Thing
Learn how encryption, encoding, and hashing can secure your data. Explore examples and differences between these essential techniques.
8 min