Hello readers 👋, welcome to the 8th blog of our Node.js journey!
So far, we've covered how Node.js works under the hood, its event loop, non-blocking code, and how to handle async operations. Now we're going to build something practical that almost every real-world application needs: user authentication using JSON Web Tokens (JWT).
Authentication is how an application knows who you are. You see it every day: logging into a website, accessing private messages, or making a purchase. Today, we'll understand what JWT is, how it works (without diving into complex cryptography), and how to implement a simple, secure login flow in Node.js.
Let's get started.
What authentication means
Authentication is the process of verifying a user's identity. When you log in with a username and password, the server checks those credentials and, if they match, it should remember that you are authenticated for subsequent requests. Without authentication, anyone could pretend to be anyone, and there would be no security.
In traditional web applications, once logged in, the server creates a session and stores it in memory or a database. The client gets a session ID stored in a cookie. Each request sends that cookie, and the server looks up the session. This works, but it's stateful: the server must store session data, which can be a scalability bottleneck if you have multiple servers.
JWT offers a stateless approach. The server doesn't need to remember anything. Instead, after a successful login, the server creates a token that contains user information and signs it. The client stores this token and sends it with every request. The server simply verifies the signature and reads the user data directly from the token. No database lookups for sessions. This makes scaling much easier.
What JWT is
JWT stands for JSON Web Token. It's an open standard (RFC 7519) that defines a compact, self-contained way to transmit information between parties as a JSON object. The information can be trusted because it's digitally signed.
JWT is perfect for authentication. After a user logs in, the server generates a JWT containing their user ID (and maybe role) and sends it to the client. The client sends the token in the Authorization header of every subsequent request. The server checks the signature and extracts the user ID, thus identifying the user without a database session lookup.
Because the token itself contains the user information, it's self-contained. That's the "stateless" magic.
Structure of a JWT
A JWT looks like a long random string of characters with two dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9.4x9MMbKd7P3qW7Y8lQZ2VcDhFfR1jNzOyU5wTkXy
This string is actually three parts, each Base64URL-encoded, separated by dots. Let's decode each part conceptually (not deep crypto, just the idea).
Header
The first part is the header. It typically contains two fields: the type of token (JWT) and the signing algorithm used (HS256 or RS256). After Base64 decoding, it looks like:
{
"alg": "HS256",
"typ": "JWT"
}
Payload
The second part is the payload. This is where the actual data (claims) about the user lives. The server puts information like user ID, username, role, and expiration time here. For example:
{
"userId": 1,
"username": "satya",
"role": "admin",
"iat": 1680000000,
"exp": 1680003600
}
The iat (issued at) and exp (expiration) claims are standard. You can add custom data too. Remember: the payload is not encrypted, just Base64-encoded. Anyone can decode it, so never put sensitive info like passwords in it.
Signature
The third part is the signature. This is what makes the token trustworthy. To create the signature, the server takes the encoded header, the encoded payload, and a secret key, then applies the algorithm specified in the header (e.g., HMAC SHA256). The result is a cryptographic hash that ensures the token hasn't been tampered with.
If a malicious user tries to change the payload (e.g., change userId from 1 to 2), the signature will no longer match when the server recalculates it using the secret key. The server detects tampering and rejects the token. That's the core security.
Login flow using JWT
Let's walk through a typical login flow step by step, with code snippets using Node.js, the express web framework (for routing convenience), and the jsonwebtoken library.
1. Install required packages:
npm install express jsonwebtoken
2. Create a simple server with a login endpoint.
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json()); // to parse JSON request body
const SECRET_KEY = 'your-secret-key-change-this'; // in production, use an env var
// In-memory user store (for demo)
const users = [
{ id: 1, username: 'satya', password: 'password123', role: 'admin' }
];
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Create a token. The payload includes the user's id and role.
// Don't include the password!
const token = jwt.sign(
{ userId: user.id, role: user.role },
SECRET_KEY,
{ expiresIn: '1h' } // token expires in 1 hour
);
res.json({ token });
});
app.listen(3000, () => console.log('Server running on port 3000'));
What's happening here:
- We receive username and password.
- We check against a hardcoded list (in reality, you'd check a database with hashed passwords).
- If valid, we use
jwt.sign()to create a token with a payload containing the user ID and role. The token is signed with our secret key and set to expire in 1 hour. - We send the token back to the client as JSON.
3. Client stores the token and sends it with requests.
After login, the client (browser or mobile app) stores the token. In a web app, you might use localStorage or a secure HTTP-only cookie. For simplicity, we'll use the Authorization header with the Bearer scheme.
A subsequent request to a protected route would include the header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Protecting routes using tokens
Now we need to protect certain API routes so that only authenticated users can access them. We'll create a middleware that checks the token.
// Middleware to verify JWT
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token missing' });
}
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
// decoded contains the payload, e.g., { userId: 1, role: 'admin', iat: ..., exp: ... }
req.user = decoded;
next(); // proceed to the next middleware/route handler
});
}
Then we can protect any route by adding this middleware:
app.get('/profile', authenticateToken, (req, res) => {
// req.user is available from the token
res.json({ message: `Hello user ${req.user.userId}, your role is ${req.user.role}` });
});
app.get('/admin', authenticateToken, (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ message: 'Admin access required' });
}
res.json({ message: 'Welcome to admin panel' });
});
How it works:
- The client sends a request to
/profilewith the token. - The
authenticateTokenmiddleware extracts the token, callsjwt.verify()with the secret key. - If valid, it attaches the decoded payload to
req.userand passes control to the actual route handler. - The handler can use
req.user.userIdor any custom claim. - If the token is invalid (bad signature, expired), the middleware sends a 403 response.
Sending token with requests: from client side
In a Node.js client (or frontend JS), you'd include the token after login:
// After login, store token
const tokenResponse = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'satya', password: 'password123' })
});
const { token } = await tokenResponse.json();
// Store token (e.g., in localStorage)
localStorage.setItem('token', token);
// Later, for protected requests:
const token = localStorage.getItem('token');
const profileResponse = await fetch('/profile', {
headers: { 'Authorization': `Bearer ${token}` }
});
In a pure HTTP context (like Postman), you'd manually set the header.
The token validation lifecycle
From the moment the server issues a token to its eventual expiry or logout, the lifecycle is:
- Issuance: Server creates and signs token on successful login.
- Storage: Client stores token (memory, localStorage, cookie).
-
Transmission: Client sends token in
Authorizationheader with each request. - Verification: Server middleware verifies signature and expiration. If valid, request proceeds; if invalid, 403.
-
Expiration: After the
expclaim time, the token is rejected even if the signature is correct. This forces the user to re-login. - Logout (optional): Since JWTs are stateless, there's no server-side invalidation. To "logout", you simply remove the token from the client. For added security, you can maintain a token blacklist (stateful), but the stateless beauty is lost.
Visualizing the flow
JWT login authentication flow:
A note on security
- Always use HTTPS: JWTs are transmitted in headers; without TLS, an attacker could steal the token.
- Keep the secret key secret: Store it in environment variables, never in code.
- Set a short expiration time: 15 minutes to 1 hour is typical. Use refresh tokens for longer sessions (beyond this article).
- Don't put sensitive data in the payload: It's only Base64-encoded, not encrypted.
-
Consider token storage:
localStorageis vulnerable to XSS attacks; HttpOnly cookies are more secure but require CSRF protection. The best approach depends on your application.
Conclusion
JWT authentication is a clean, stateless way to secure Node.js APIs. It eliminates server-side session storage, simplifies scaling, and gives you a self-contained token that carries user identity. By understanding the header, payload, and signature, you now know exactly why JWTs are secure and how to implement the full login and route protection flow.
Let's quickly recap:
- Authentication verifies identity; JWT provides a stateless method by issuing a signed token.
- A JWT consists of three Base64-encoded parts: header (algorithm), payload (user data), and signature (tamper-proof).
- The login endpoint validates credentials and returns a JWT made with
jsonwebtoken.sign(). - The client sends the token in the
Authorization: Bearer <token>header for subsequent requests. - Middleware verifies the token with
jwt.verify(), extracts the user payload, and protects routes. - This flow is scalable, simple, and widely adopted.
With JWT authentication in your toolbox, you can now build secure, stateless Node.js backends. In the next post, we'll continue building practical features. See you then!
Hope you found this helpful! If you spot any mistakes or have suggestions, let me know. You can find me on LinkedIn and X, where I post more about web development.

Top comments (0)