JWT: What Problem Is It Actually Solving?
Understanding JWT from first principles by starting with the problem HTTP authentication needs to solve.
JWT: What Problem Is It Actually Solving?
I was looking at the authentication code in my Spring Boot application and kept seeing JWT everywhere.
JwtEncoder, JwtDecoder, BearerToken, claims, signatures…
At first, it felt like JWT was this big security concept that I had to understand before I could understand the code.
But when I went back to the actual problem, the idea turned out to be much simpler.
Let’s start from the beginning.
The problem
Suppose we have a login endpoint:
POST /auth/login
│
▼
email + password
│
▼
find user in DB
│
▼
check BCrypt password
│
▼
login successful
Nothing unusual here.
The user sends their email and password, we find the user, compare the password with the BCrypt hash stored in the database, and if everything matches, the login succeeds.
But now the frontend makes another request:
GET /applications
How does the server know that this is the same user who just logged in?
That’s the interesting part.
HTTP doesn’t remember that the previous request was successful.
From the server’s point of view, these are separate requests:
POST /auth/login
GET /applications
GET /jobs
POST /applications
The server doesn’t get some magical information saying:
"Oh, this person logged in 30 seconds ago."
We need to send some kind of proof with the next request.
That’s the problem JWT is helping us solve.
So what happens after login?
After the email and password have been verified, the server creates a token.
email + password
│
▼
verify in DB
│
▼
valid
│
▼
create JWT
│
▼
return JWT to client
The client can then send that token with future requests.
Something like:
GET /applications
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
Now the server has something to work with.
Instead of asking the user for their password again, it can check the token.
Conceptually:
JWT
│
├── Is the signature valid?
├── Has it been modified?
├── Has it expired?
└── Who does it belong to?
If everything checks out, the server can treat the request as authenticated.
That’s really the core idea.
JWT isn’t the login itself.
The password is still used during login.
JWT is what we give the client after successful authentication so it can identify itself on later requests.
Okay, but what actually is a JWT?
JWT stands for JSON Web Token.
A JWT usually looks something like this:
xxxxx.yyyyy.zzzzz
Those three sections are:
HEADER.PAYLOAD.SIGNATURE
For example:
eyJhbGciOiJIUzI1NiJ9
.
eyJzdWIiOiJzYXVyYWJoQGV4YW1wbGUuY29tIiwicm9sZSI6IkNBTkRJREFURSJ9
.
abc123...
There are three things in there:
- Header
- Payload
- Signature
The interesting part is understanding why each one exists.
Header
The header contains information about the token.
For example:
{
"alg": "HS256",
"typ": "JWT"
}
alg means the algorithm.
typ means the type of token.
Here we’re using HS256.
You generally don’t sit there writing this header yourself. The JWT library takes care of constructing it.
In our Spring Boot application, JJWT does this work for us.
Payload
The payload is where things get more interesting.
It contains claims.
For example:
{
"sub": "saurabh@example.com",
"userId": 1,
"role": "CANDIDATE",
"iat": 1791225000,
"exp": 1791228600
}
These are basically pieces of information that the token is carrying.
For example:
sub → who is this token about?
userId → which user?
role → what role does the user have?
iat → when was the token issued?
exp → when does it expire?
In code, this might look something like:
.subject(email)
.claim("userId", userId)
.claim("role", role)
.issuedAt(issuedAt)
.expiration(expiration)
So when I call:
.claim("role", "CANDIDATE")
I’m basically saying:
Put
role = CANDIDATEinto the token.
That’s what a claim is.
It’s just information attached to the token.
But can’t the user change the payload?
This is where the signature comes in.
Imagine I receive a token containing:
{
"userId": 1,
"role": "CANDIDATE"
}
What would stop me from changing it to:
{
"userId": 1,
"role": "ADMIN"
}
If JWT were just an encoded JSON object, nothing would stop me.
That’s why the token also contains a signature.
Conceptually, the server does something like:
HEADER
+
PAYLOAD
+
SECRET KEY
│
▼
SIGNATURE
The server has a secret key that the client doesn’t have.
When creating the JWT, the server uses the header and payload along with that key to produce the signature.
So we end up with:
HEADER.PAYLOAD.SIGNATURE
Later, when the client sends the JWT back, the server can verify the signature.
If somebody changed:
role = CANDIDATE
to:
role = ADMIN
the payload would no longer correspond to the original signature.
The server detects that and rejects the token.
That’s the useful part of the signature.
It’s essentially protecting the token from being modified without detection.
JWT is not encryption
This is probably the most important thing to remember.
A JWT payload is normally encoded, not encrypted.
So if your JWT contains:
{
"userId": 1,
"role": "CANDIDATE"
}
don’t assume that this information is secret.
Someone who has the token can decode the payload.
The signature doesn’t hide the payload.
It tells the server:
“This data hasn’t been changed since it was signed.”
That’s a very different thing from encryption.
So I wouldn’t put things like:
password
passwordHash
credit card information
private secrets
inside a JWT.
A reasonable payload might look more like:
{
"sub": "saurabh@example.com",
"userId": 1,
"role": "CANDIDATE",
"exp": 1791228600
}
The information isn’t meant to be a secret.
The important part is that the client shouldn’t be able to modify it and make the server believe the modified version is legitimate.
Encoding, encryption and signing are different
These three are easy to mix up.
Encoding changes the representation of data.
JSON
│
▼
Base64URL
You don’t need a secret key to decode it.
Encryption is about confidentiality.
data + key
│
▼
encrypted data
Without the appropriate key, the original data shouldn’t be readable.
Signing is about detecting modification and establishing that the data was produced under the expected signing key.
data + signing key
│
▼
signature
JWTs commonly use signing.
So a normal JWT gives us something roughly like:
readable payload
+
tamper detection
not:
hidden payload
Putting the whole thing together
Now the original login flow makes more sense.
The user sends:
POST /auth/login
Content-Type: application/json
{
"email": "saurabh@example.com",
"password": "mypassword"
}
The server verifies the credentials:
email + password
│
▼
find user
│
▼
BCrypt verification
│
▼
valid
│
▼
create JWT
The JWT might contain:
{
"sub": "saurabh@example.com",
"userId": 1,
"role": "CANDIDATE",
"exp": 1791228600
}
The server returns it to the client.
Then the client makes:
GET /applications
Authorization: Bearer <JWT>
The server receives the token and checks it.
Something like:
Authorization header
│
▼
extract JWT
│
▼
verify signature
│
▼
check expiration
│
▼
read claims
│
▼
identify user
│
▼
process request
And that’s JWT.
At least, that’s the part I think is worth understanding before diving into Spring Security.
The framework code can look complicated because there are a lot of moving pieces around this process.
But underneath all of it, the problem is pretty small:
User logs in
│
▼
Server verifies credentials
│
▼
Server gives client a signed token
│
▼
Client sends token on future requests
│
▼
Server verifies token
│
▼
Server knows who is making the request
It’s just a way of carrying verifiable authentication information from one HTTP request to the next.