Lesson 44 of 50 – JWT Authentication
88% Completed
88%

JWT Authentication in Node.js

Learn how JSON Web Tokens work, how to generate and verify JWTs, and how to protect Express.js API routes using token-based authentication.

What is JWT? JWT stands for JSON Web Token. It is a compact token format used to transfer claims between parties. In web applications, JWTs are commonly used to represent authentication information.

1. What Is JWT Authentication?

JWT authentication allows a server to issue a signed token after successfully verifying a user's credentials. The client can then send that token with later requests to access protected routes.

JWT Authentication Flow
  1. The user submits an email and password.
  2. The server verifies the user's credentials.
  3. The server creates a signed JWT.
  4. The client receives the token.
  5. The client sends the token with a protected request.
  6. The server verifies the token before returning private data.

2. What Does a JWT Look Like?

A typical signed JWT contains three parts separated by dots:

header.payload.signature
Part Purpose
Header Contains token metadata, such as the signing algorithm.
Payload Contains claims, such as a user ID and expiration time.
Signature Helps verify that the token has not been modified and was signed with the expected key.
Security note: A normally signed JWT is not encrypted. Its header and payload can be decoded by anyone who has the token. Never put passwords, private keys, or other sensitive secrets in its payload.

3. Install the Required Packages

Create a Node.js project if you do not already have one:

mkdir node-jwt-app
cd node-jwt-app
npm init -y

Install Express, JWT support, password hashing, and environment configuration:

npm install express jsonwebtoken bcryptjs dotenv
Package Explanation
Package Purpose
express Creates the server and API routes.
jsonwebtoken Signs and verifies JWTs.
bcryptjs Hashes passwords and checks submitted passwords.
dotenv Loads environment variables from a local file.

4. Create a JWT Secret

A secret key is used to sign and verify tokens when using a symmetric algorithm such as HS256.

Generate a strong random secret using Node.js:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Create a .env file:

JWT_SECRET=replace_with_your_generated_random_secret
PORT=5000

Replace the placeholder with the generated value. Keep the .env file private and do not commit it to source control.

.env
node_modules/
Never publish your JWT secret or include it in frontend JavaScript. Anyone who obtains a symmetric signing secret may be able to create valid tokens.

5. Generate a JWT Token

The jwt.sign() method creates a signed token from a payload and signing key.

require("dotenv").config();

const jwt = require("jsonwebtoken");

if (!process.env.JWT_SECRET) {
    throw new Error("JWT_SECRET must be configured");
}

const token = jwt.sign(
    { userId: 101 },
    process.env.JWT_SECRET,
    {
        algorithm: "HS256",
        expiresIn: "1h"
    }
);

console.log(token);
Explanation
  • jwt.sign() creates a signed JWT.
  • userId identifies the user in this example.
  • JWT_SECRET is the signing secret.
  • algorithm: "HS256" explicitly selects the signing algorithm.
  • expiresIn: "1h" makes the token expire after one hour.

6. Verify a JWT Token

Use jwt.verify() to check the token's signature, expiration, and other applicable registered claims.

require("dotenv").config();

const jwt = require("jsonwebtoken");

const token = jwt.sign(
    { userId: 101 },
    process.env.JWT_SECRET,
    {
        algorithm: "HS256",
        expiresIn: "1h"
    }
);

try {
    const decoded = jwt.verify(
        token,
        process.env.JWT_SECRET,
        {
            algorithms: ["HS256"]
        }
    );

    console.log(decoded);
} catch (error) {
    console.log("Invalid or expired token");
}
Example Decoded Payload
{
    userId: 101,
    iat: 1234567890,
    exp: 1234571490
}

The iat and exp values represent token issue and expiration times as Unix timestamps. The numbers above are illustrative.

Do not use jwt.decode() alone to authorize requests. Decoding does not verify the signature. Use jwt.verify() before trusting token claims.

7. Create a Practice User Store

For this beginner example, we will store one practice account in memory. A real application should use a database and enforce unique accounts at the database level.

Create users.js:

const bcrypt = require("bcryptjs");

const users = [
    {
        id: 1,
        name: "Rahul Kumar",
        email: "rahul@example.com",
        passwordHash: bcrypt.hashSync("ExamplePass123", 12)
    }
];

module.exports = users;
This account is only for local practice. Do not use example credentials in a deployed application.

8. Create a Complete JWT Authentication API

Create a file named server.js. It includes a login route, JWT creation, authentication middleware, and a protected profile route.

server.js
require("dotenv").config();

const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const users = require("./users");

const app = express();

app.use(express.json());

if (!process.env.JWT_SECRET) {
    throw new Error("JWT_SECRET must be configured");
}

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

        if (
            typeof email !== "string" ||
            typeof password !== "string"
        ) {
            return res.status(400).json({
                message: "Email and password are required"
            });
        }

        const normalizedEmail = email.trim().toLowerCase();

        const user = users.find(
            item => item.email === normalizedEmail
        );

        if (!user) {
            return res.status(401).json({
                message: "Invalid email or password"
            });
        }

        const passwordMatches = await bcrypt.compare(
            password,
            user.passwordHash
        );

        if (!passwordMatches) {
            return res.status(401).json({
                message: "Invalid email or password"
            });
        }

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

        res.json({
            message: "Login successful",
            token: token,
            user: {
                id: user.id,
                name: user.name,
                email: user.email
            }
        });
    } catch (error) {
        console.error("Login error:", error.message);

        res.status(500).json({
            message: "Unable to log in"
        });
    }
});

// JWT authentication middleware
function authenticateToken(req, res, next) {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith("Bearer ")) {
        return res.status(401).json({
            message: "Authentication token required"
        });
    }

    const token = authHeader.slice(7).trim();

    if (!token) {
        return res.status(401).json({
            message: "Authentication token required"
        });
    }

    try {
        const decoded = jwt.verify(
            token,
            process.env.JWT_SECRET,
            {
                algorithms: ["HS256"]
            }
        );

        req.userId = decoded.userId;

        next();
    } catch (error) {
        return res.status(401).json({
            message: "Invalid or expired token"
        });
    }
}

// Protected profile route
app.get("/profile", authenticateToken, (req, res) => {
    const user = users.find(
        item => item.id === req.userId
    );

    if (!user) {
        return res.status(401).json({
            message: "User no longer exists"
        });
    }

    res.json({
        id: user.id,
        name: user.name,
        email: user.email
    });
});

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

9. Run the Application

Start the server from your project folder:

node server.js

Expected output:

Server running on port 5000

10. Login Using Postman

Send a POST request to:

http://localhost:5000/login

Choose Body → raw → JSON and enter:

{
    "email": "rahul@example.com",
    "password": "ExamplePass123"
}
Expected Response
{
    "message": "Login successful",
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "user": {
        "id": 1,
        "name": "Rahul Kumar",
        "email": "rahul@example.com"
    }
}

The token shown above is illustrative. Your server generates a different token when you log in.

11. Access a Protected Route

After logging in, copy the returned token and send a GET request to:

http://localhost:5000/profile

In Postman:

  1. Open the Authorization tab.
  2. Select Bearer Token.
  3. Paste the token received from the login endpoint.
  4. Click Send.
Expected Response
{
    "id": 1,
    "name": "Rahul Kumar",
    "email": "rahul@example.com"
}

Without a valid token, the protected endpoint returns 401 Unauthorized.

12. Send a JWT from JavaScript

A frontend application can send a token in the Authorization header.

const token = "YOUR_TOKEN_FROM_LOGIN";

fetch("http://localhost:5000/profile", {
    method: "GET",
    headers: {
        "Authorization": "Bearer " + token
    }
})
.then(response => {
    if (!response.ok) {
        throw new Error("Request failed: " + response.status);
    }

    return response.json();
})
.then(data => console.log(data))
.catch(error => console.error(error));
Treat tokens as credentials. Avoid storing long-lived sensitive tokens in browser localStorage when a safer design is available. For browser applications, consider secure, HttpOnly cookies or an appropriate backend-for-frontend design and protect against CSRF where applicable.

13. JWT vs Session Authentication

JWT Authentication Session Authentication
Often sends a signed token with each request. Usually sends a session ID in a cookie.
Token claims can be verified using a signing key. The server looks up the session in a session store.
Revocation may require additional design, such as short expiry or a denylist. The server can invalidate a session by destroying it.
Useful for some distributed API architectures. Useful for traditional web applications and server-managed login state.
JWT is not automatically more secure than sessions. Choose an authentication approach based on your application's requirements, storage strategy, revocation needs, and security model.

14. Common JWT Errors

Error Possible Solution
Token required Send the token using the Authorization header.
Invalid token Check that the token is complete and signed with the expected secret.
Token expired Authenticate again to obtain a fresh token.
JWT_SECRET missing Configure the secret in the environment.
Cannot find module Run npm install jsonwebtoken.
401 Unauthorized Verify the login credentials and send a valid token.

15. JWT Security Best Practices

  • Use a strong, randomly generated signing secret.
  • Keep signing secrets on the server.
  • Set an appropriate token expiration time.
  • Verify signatures and explicitly allow expected algorithms.
  • Do not store passwords or other secrets in the token payload.
  • Use HTTPS in production.
  • Validate token claims and permissions before granting access.
  • Plan for token revocation and account logout where needed.
  • Use refresh tokens only with a carefully designed rotation and revocation strategy.
  • Apply rate limiting to login and token-related endpoints.

16. Practical Assignment

JWT Authentication API

Create a Node.js API that uses JWT authentication.

  1. Create a login endpoint.
  2. Verify a user's password using bcrypt.
  3. Generate a JWT after successful login.
  4. Set an expiration time for the token.
  5. Create JWT authentication middleware.
  6. Protect a student profile endpoint.
  7. Test the API with and without a token in Postman.
  8. Return an appropriate status code for invalid or expired tokens.

Bonus Task: Add a role claim and restrict an admin-only route. Remember that authorization must be checked on the server.

17. Lesson Summary

In this lesson, you learned what JWTs are, how to generate and verify tokens, how to create login endpoints, how to protect Express routes with authentication middleware, and how to test JWT authentication using Postman.

Test Your Knowledge – JWT Authentication

Select the correct answer for each question.

1. What does JWT stand for?
2. Which method verifies a JWT using jsonwebtoken?
3. Where should a JWT signing secret be stored?