You enter your email and password, click Login, and suddenly the application knows who you are.
A few seconds later, you open your profile and the backend already knows which user's data to return.
But HTTP doesn't work like that by itself.
Every request is independent. So after the login request finishes, how does the server recognize the next request?
One popular solution is JWT authentication.
JWT stands for JSON Web Token. It gives the client a signed piece of information that can be sent with later requests so the server can verify the request's identity.
A simplified flow looks like this:
Login
↓
Verify credentials
↓
Create JWT
↓
Send JWT to client
↓
Client sends JWT with API requests
↓
Server verifies JWT
↓
Request continues
The interesting part is what happens inside that flow.
Why Does Authentication Need a Token?
Imagine you log into an application:
POST /api/login
with:
{
"email": "alex@example.com",
"password": "mypassword"
}
The backend verifies the credentials.
At this point, it knows:
This is user 42.
But the next request could be:
GET /api/profile
There is nothing inherent in HTTP that tells the server:
"This is the same person who successfully logged in two seconds ago."
The application needs a way to carry that authentication information from one request to another.
That's where the token comes in.
After successful authentication, the server issues a JWT:
Login
↓
JWT issued
↓
Client keeps JWT
↓
JWT sent with future requests
The token becomes part of the application's authentication mechanism.
What Is Inside a JWT?
A JWT usually looks like this:
xxxxx.yyyyy.zzzzz
There are three sections:
Header.Payload.Signature
For example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiI0MiIsInJvbGUiOiJ1c2VyIn0
.
signature
These three parts have different jobs.
Header
The header describes the token.
A simplified example:
{
"alg": "HS256",
"typ": "JWT"
}
alg specifies the signing algorithm and typ identifies the token type.
Payload
The payload contains claims.
For example:
{
"sub": "42",
"role": "user",
"iat": 1760000000,
"exp": 1760003600
}
Here:
sub → subject / user identifier
role → application-specific role
iat → issued-at time
exp → expiration time
These claims allow the server to carry useful information about the token and its subject.
There is an important security detail here.
A normal JWT payload is not encrypted.
It is encoded so that the token can be represented as a compact string.
Anyone who obtains the token can generally decode its header and payload.
So don't put things like passwords, private keys, or other secrets inside the payload.
Signature
The signature is what protects the integrity of the signed token data.
Conceptually:
Header + Payload
↓
Signing algorithm
+
Secret/private key
↓
Signature
The server can later verify the signature.
If someone changes:
{
"role": "user"
}
to:
{
"role": "admin"
}
without having the appropriate signing key, the signature will no longer match.
The server can reject the token.
So the signature doesn't hide the payload.
It helps answer:
Has the signed token data been changed, and does it have a valid signature?
What Happens When You Log In?
Let's follow the request from beginning to end.
The client sends:
POST /api/login
Content-Type: application/json
with:
{
"email": "alex@example.com",
"password": "mypassword"
}
The backend might do something like:
Receive credentials
↓
Find user
↓
Verify password
↓
Credentials valid?
↓
Create JWT
↓
Sign JWT
↓
Return token
The token might contain:
{
"sub": "42",
"exp": 1760003600
}
The server signs it using the configured key.
The response could then contain:
{
"accessToken": "eyJ..."
}
The client can now use that access token when calling protected APIs.
The Next API Request
Now the frontend wants Alex's profile.
It sends:
GET /api/profile
Authorization: Bearer eyJ...
The backend receives the request and extracts the token.
It then verifies it.
Conceptually:
Request
↓
Extract JWT
↓
Verify signature
↓
Check expiration
↓
Validate required claims
↓
Identify user
↓
Continue request
If the token represents user 42, the application can use that identity when processing the request.
For example:
JWT
↓
sub = 42
↓
User 42
↓
Fetch profile
↓
Return profile
Notice something important here.
The user doesn't send their password with every request.
The JWT is the credential being presented for that request.
JWT Authentication in Express
In a Node.js application, JWT verification is often placed in middleware.
A simplified example:
function authenticate(req, res, next) {
const header = req.headers.authorization;
if (!header || !header.startsWith("Bearer ")) {
return res.status(401).json({
message: "Authentication required"
});
}
const token = header.slice(7);
try {
const payload = jwt.verify(
token,
process.env.JWT_SECRET
);
req.user = payload;
next();
} catch {
return res.status(401).json({
message: "Invalid or expired token"
});
}
}
A protected route can then use the middleware:
app.get(
"/api/profile",
authenticate,
getProfile
);
The request now has a predictable path:
GET /api/profile
↓
Authentication middleware
↓
Verify JWT
↓
Attach user to request
↓
Controller
↓
Database
↓
Response
The controller doesn't need to repeatedly implement JWT verification.
That responsibility stays in the authentication layer.
Authentication Is Still Different From Authorization
This is where many beginners mix things up.
Suppose the JWT tells the application that the current user is:
User ID: 42
Role: user
That establishes identity.
But now the user requests:
DELETE /api/users/100
The backend still has another question:
Is user 42 allowed to delete another user?
That's authorization.
So the complete process is:
JWT verification
↓
Who is this?
↓
User 42
↓
Authorization check
↓
Can User 42 perform this action?
↓
Allow / Reject
For example:
function requireAdmin(req, res, next) {
if (req.user.role !== "admin") {
return res.status(403).json({
message: "Forbidden"
});
}
next();
}
And:
app.delete(
"/api/users/:id",
authenticate,
requireAdmin,
deleteUser
);
JWT helps with authentication.
It does not automatically give the user permission to do everything.
What Does Stateless JWT Authentication Mean?
You've probably heard that JWT authentication is stateless.
The idea is that the server doesn't necessarily need to maintain a traditional server-side session for every access token.
With a session-based approach:
Client
↓
Session ID
↓
Server
↓
Session Store
↓
User
With a self-contained JWT:
Client
↓
JWT
↓
Server verifies token
↓
Claims identify subject
This can be convenient in systems with multiple backend instances.
Imagine:
Load Balancer
↓
┌───────────┼───────────┐
↓ ↓ ↓
Server A Server B Server C
↑ ↑ ↑
└──── Verify JWT ───────┘
Each server can verify the token using the appropriate key.
There is no requirement for every request to first find a traditional session record.
But there's a catch.
Stateless doesn't mean there is no state anywhere in the authentication system.
Applications may still maintain state for refresh tokens, revocation, device sessions, security events, or other security controls.
Access Tokens and Refresh Tokens
Access tokens are often short-lived.
For example:
Access Token
↓
Expires in 15 minutes
If the access token expires, the user shouldn't necessarily have to log in again.
A common design is to use a refresh token.
Login
↓
Access Token + Refresh Token
↓
Access Token used for APIs
↓
Access Token expires
↓
Refresh Token
↓
New Access Token
This gives the application a way to keep a user signed in while limiting the lifetime of an individual access token.
Refresh tokens need careful handling because a stolen refresh token can have a much longer useful lifetime.
That's why real authentication systems often add things like refresh-token rotation and revocation.
What Happens When the JWT Expires?
JWTs commonly contain an expiration claim:
{
"sub": "42",
"exp": 1760003600
}
Once that time has passed, the token should no longer be accepted as a valid access token.
The API might respond with:
401 Unauthorized
The client can then use its refresh mechanism if one exists:
API request
↓
Access token expired
↓
401
↓
Refresh authentication
↓
New access token
↓
Retry request
This is one reason short-lived access tokens are useful.
If an access token is stolen, its useful lifetime can be limited.
It doesn't make theft harmless, but it can reduce the window of exposure.
Where Should You Store a JWT?
There isn't one universal answer.
JWTs are commonly transported using:
Authorization header
or:
Cookie
For browser applications, cookies can provide useful security controls.
For example:
Set-Cookie: access_token=...;
HttpOnly;
Secure;
SameSite=Lax
HttpOnly prevents normal page JavaScript from directly reading the cookie.
Secure tells the browser to send it only over HTTPS.
SameSite controls when the browser sends the cookie in cross-site situations.
Cookies aren't automatically safer in every situation, though. They introduce considerations such as CSRF protection and cross-origin configuration.
The right approach depends on the application's architecture and threat model.
What If Someone Steals the JWT?
A valid access token should be treated like a credential.
If an attacker gets one, they may be able to make authenticated API requests until the token expires or is otherwise invalidated.
For example:
Attacker
↓
Stolen JWT
↓
Authorization: Bearer <token>
↓
API
↓
Authenticated request
That's why protecting the token matters.
Common security practices include:
HTTPS
Short-lived access tokens
Secure refresh-token handling
Signature verification
Expiration checks
Issuer/audience validation when applicable
Restricted signing algorithms
Avoiding sensitive information in payloads
JWT doesn't provide all of these protections automatically.
The application has to implement the authentication system correctly.
JWT Is Not Encryption
This is probably the most common JWT misconception.
People see:
Header.Payload.Signature
and assume the contents are hidden.
They're not.
A normal signed JWT provides integrity, not confidentiality.
Think of it this way:
Encoding
→ Makes data representable as a token
Signing
→ Helps detect modification
Encryption
→ Hides the data
JWT signing and encryption solve different problems.
If the application needs sensitive information to remain confidential, it needs an appropriate encryption mechanism rather than simply putting that information into a signed JWT.
401 vs 403
JWT authentication also makes the difference between 401 and 403 easier to understand.
A 401 Unauthorized response generally means the request doesn't have valid authentication credentials.
For example:
No token
Invalid token
Expired token
In simple terms:
401
→ "I can't authenticate you."
A 403 Forbidden response means the server knows who the user is, but the user isn't allowed to perform the requested action.
Authenticated user
↓
Not enough permissions
↓
403
So:
401 → Authentication problem
403 → Authorization problem
JWT Doesn't Replace Good API Security
It can be tempting to think:
"We're using JWT, so our API is secure."
That's not how it works.
JWT only solves part of the problem.
A production authentication system still needs to think about:
Password storage
HTTPS
Token lifetime
Refresh tokens
Token theft
Revocation
Key management
Authorization
CSRF
XSS
Rate limiting
Account recovery
Brute-force protection
The exact list depends on the application.
JWT is a tool, not the entire security architecture.
JWT vs Sessions
JWT and sessions are often compared as if one has to replace the other.
In reality, both are valid approaches.
A session-based system might look like:
Login
↓
Create session
↓
Session ID in cookie
↓
Server looks up session
A JWT-based system might look like:
Login
↓
Create signed JWT
↓
Client receives token
↓
Client sends token
↓
Server verifies token
Sessions make some server-side operations, such as immediate session invalidation, straightforward.
JWTs can be useful when authentication information needs to be verified across multiple services without relying on a shared session store.
But JWT systems can become complicated once you add refresh tokens, revocation, key rotation, and multiple token types.
So don't choose JWT simply because it is popular.
Start with the requirements of the application.
The Complete JWT Flow
Putting everything together:
LOGIN
↓
Email + Password
↓
Backend
↓
Verify credentials
↓
Create JWT
↓
Sign JWT
↓
Send to client
↓
Future API request
↓
Send JWT with request
↓
Verify JWT
↓
Validate its claims
↓
Identify user
↓
Check permissions
↓
Business logic
↓
Database
↓
Response
There are really two separate decisions happening:
Authentication
↓
"Who is making this request?"
Authorization
↓
"What is this user allowed to do?"
A Practical Mental Model
When you encounter JWT authentication in a project, think about it in this order:
1. User logs in
↓
2. Backend verifies credentials
↓
3. Backend issues a signed JWT
↓
4. Client sends the JWT with API requests
↓
5. Backend verifies the token
↓
6. Backend identifies the user
↓
7. Authorization checks permissions
↓
8. Request is allowed or rejected
And remember what each part actually means:
JWT
├── Header
│ └── Token metadata
│
├── Payload
│ └── Claims
│
└── Signature
└── Integrity/authenticity check
The easiest mistake to make is thinking that JWT is simply a secure replacement for a password.
It isn't.
A JWT is a signed token carrying claims. The server uses it to authenticate requests without requiring the user's password on every request.
Once you understand that, the rest of the system starts making sense.
Login creates the authentication context.
The JWT carries that context between requests.
The server verifies the JWT.
Authorization decides what the authenticated user can actually do.
That's the core of JWT authentication.
Top comments (0)