Skip to main content
Madhukar
All Engineering Articles
Language:
Node.js & Backend•6 min read•Sep 12, 2026•By Mr Madhukar

JWT Authentication in Node.js Explained Simply

Why authentication is required Without authentication, a server has no way to answer a basic question: who is making this request? Anyone could claim to be anyone — viewing another...

Cover illustration for JWT Authentication in Node.js Explained Simply
Share Article

Why authentication is required

Without authentication, a server has no way to answer a basic question: who is making this request? Anyone could claim to be anyone — viewing another user’s data, modifying an account that isn’t theirs, or accessing something meant to be private. Authentication exists to answer that question reliably, and JWT (JSON Web Token) is one of the most common, practical ways to do it in modern Node.js applications.

What Authentication Means

Authentication is the process of verifying who is making a request — typically by checking credentials (a username and password) once, at login, and then issuing some form of proof that can be checked on every request afterward, without asking for the password again each time.

What JWT Is

A JWT (JSON Web Token) is a compact, self-contained token that holds identity information directly inside it, cryptographically signed so it can’t be tampered with — as covered conceptually in this series’s dedicated sessions-vs-JWT comparison article.

The key idea that makes JWT particularly useful: it enables stateless authentication. The server doesn’t need to store anything about who’s logged in — everything needed to verify a request is contained within the token itself, checked purely by validating its signature.

Structure of a JWT

A JWT is made up of three parts, separated by dots: header.payload.signature.

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjQyfQ.4f8a2c...

The header identifies the token’s type and the signing algorithm used:

{
"alg": "HS256",
"typ": "JWT"
}

Payload

The payload holds the actual data — often called claims — such as the user’s ID or role. This part is readable by anyone (it’s just encoded, not encrypted), so it should never contain sensitive secrets like a password.

{
"userId": 42,
"role": "admin"
}

Signature

The signature is generated by combining the header, the payload, and a secret key known only to the server, then running them through a signing algorithm. This is what lets the server verify the token hasn’t been tampered with — any change to the header or payload would produce a completely different signature.

Login Flow Using JWT

const jwt = require("jsonwebtoken");

app.post("/login", async (req, res) => {
const { email, password } = req.body;

const user = await findUserByEmail(email);
const isValid = user && (await comparePasswords(password, user.hashedPassword));

if (!isValid) {
return res.status(401).json({ error: "Invalid credentials" });
}

const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);

res.json({ token });
});

Once the credentials check out, the server signs a new JWT containing the user’s identity, and sends it back — that token is now the client’s proof of authentication for future requests.

Sending Token with Requests

The client stores the token (commonly in memory, a cookie, or occasionally local storage, with cookies generally being the safer default) and includes it on subsequent requests — typically in the Authorization header:

fetch("/api/profile", {
headers: {
Authorization: `Bearer ${token}`,
},
});

The Bearer prefix is a widely used convention indicating "the following value is a bearer token" — a credential that grants access to whoever presents it, similar in spirit to a ticket that doesn't require further proof of identity beyond simply holding it.

Protecting Routes Using Tokens

Middleware (as covered in depth in this series’s dedicated Express middleware article) is the natural place to verify a token before allowing a request through:

function authenticateToken(req, res, next) {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(" ")[1];

if (!token) {
return res.status(401).json({ error: "No token provided" });
}

jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ error: "Invalid or expired token" });
}
req.user = decoded; // attach the decoded payload for later use
next();
});
}

app.get("/api/profile", authenticateToken, (req, res) => {
res.json({ userId: req.user.userId, role: req.user.role });
});

Token validation lifecycle

Because verification only requires checking the signature against the server’s secret key — no database lookup, no stored session to check — this scales naturally across multiple servers without needing any shared session storage.

Final Takeaway

JWT authentication answers the “who is making this request” question without the server needing to remember anything between requests. The header and payload describe the token and its claims; the signature proves neither has been tampered with, using a secret only the server knows. Login issues a signed token once; the client attaches it to every future request via the Authorization header; and middleware verifies that signature before letting a request reach any protected route. Once this flow — issue, attach, verify — feels natural, JWT stops being an abstract acronym and becomes a genuinely straightforward pattern you can implement confidently in any Node.js application.

Frequently Asked Questions

Is the data inside a JWT payload encrypted?

> No — the payload is encoded, not encrypted, meaning anyone can read it without the secret key. Never put sensitive data like passwords directly in a JWT payload; the signature protects against tampering, not against the contents being visible.

What happens when a JWT expires?

> jwt.verify() will fail with an expiration error, and the request should be rejected (typically with a 401 or 403 status) — the client would then need to log in again, or use a separate refresh-token mechanism to obtain a new token, a more advanced pattern worth exploring once this basic flow feels solid.

Where should the JWT secret key be stored?

> In an environment variable (like process.env.JWT_SECRET), never hardcoded directly in your source code or committed to version control — since anyone with access to that secret could forge valid tokens.

Can I store user roles or permissions directly in the JWT payload?

> Yes, and it’s a common, practical pattern — as shown in the examples above. Just keep in mind that if a user’s role changes after a token was issued, the old token still reflects the old role until it expires, which is a tradeoff worth considering for sensitive permission changes.

JWT Authentication in Node.js Explained Simply was originally published in Towards Dev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Share Article

Originally published by Mr Madhukar

Read the complete article on Medium with full formatting & reader responses.