JWT Authentication Code with use cases

Introduction

Authentication is one of the most important parts of a modern web application. Whenever an application needs to identify a user and restrict access to certain resources, it needs an authentication mechanism.

One popular approach for API authentication is JWT (JSON Web Token).

JWT authentication is commonly used with:

  • Node.js applications
  • Express.js REST APIs
  • Single-page applications
  • Mobile applications
  • Microservices
  • Frontend and backend applications
  • Protected API endpoints

In this tutorial, we will understand how JWT authentication works in Node.js, how to generate a token after login, how to verify that token, and how middleware can protect private API routes.

We will also walk through a practical Express.js example involving user registration, login, states, districts, and protected APIs.


What Is JWT?

JWT stands for JSON Web Token.

It is a compact token format commonly used to securely transmit claims between parties.

In a typical authentication flow, a user provides a username and password. If the credentials are valid, the server creates a JWT and sends it back to the client.

The client can then send that token with subsequent requests to protected APIs.

A simplified flow looks like this:

User
  |
  | Username + Password
  v
Login API
  |
  | Verify credentials
  v
JWT Token
  |
  | Send token with future requests
  v
Protected API
  |
  | Verify JWT
  v
Access Granted

The important point is that a JWT is a token representing authenticated claims; it is not a password and should not be treated as encrypted user credentials.


Why Is JWT Authentication Used?

Without authentication, a public API endpoint may allow anyone to access or modify data.

For example:

GET /states

might be accessible to every visitor.

But an administrative operation such as:

DELETE /districts/10

should normally be restricted to authenticated users.

JWT provides a convenient way for the client to prove that it has previously authenticated.


JWT Authentication Flow

A typical JWT-based authentication system works in the following stages.

Step 1: User Registration

The user submits information such as:

{
  "username": "john",
  "password": "mypassword"
}

The server should hash the password before storing it in the database.


Step 2: User Login

The user submits the username and password:

username + password

The server:

  1. Finds the user.
  2. Retrieves the stored password hash.
  3. Compares the supplied password with the hash.
  4. Creates a JWT if the password is correct.


Step 3: Server Creates JWT

The server creates a token containing claims such as the username or user ID.

For example:

const payload = {
    username: username
}

The JWT library signs this payload using a secret key.


Step 4: Client Stores the Token

The client receives the token.

Depending on the application architecture, the token may be stored using an appropriate browser or application storage strategy.

For browser applications, storage decisions should account for XSS and CSRF risks rather than blindly storing sensitive tokens in localStorage.


Step 5: Client Calls a Protected API

For a protected endpoint, the client sends the token in the HTTP Authorization header:

Authorization: Bearer <JWT_TOKEN>

The word Bearer indicates that the token is being presented as the credential for the request.


Step 6: Server Verifies the Token

The Express application extracts the token and verifies its signature.

If verification succeeds, the request continues.

If verification fails, the server returns:

401 Unauthorized

JWT Structure

A JWT generally contains three sections:

Header.Payload.Signature

They are separated by dots.

Conceptually:

xxxxx.yyyyy.zzzzz

The three components are:

Header

Contains information about the token, such as the signing algorithm.

Payload

Contains claims.

For example:

{
  "username": "john"
}

Signature

The signature helps the server verify that the token was signed with the expected secret or key.

An important security concept is that the JWT payload is encoded, not automatically encrypted. Therefore, you should not put passwords, secret keys, or other sensitive information inside the payload.


Required Node.js Packages

This example uses several npm packages:

const express = require('express')
const bcrypt = require('bcrypt')
const path = require('path')
const {open} = require('sqlite')
const sqlite3 = require('sqlite3')
const jwt = require('jsonwebtoken')

Each package has a different purpose.

Express

const express = require('express')

Express is used to create the HTTP server and REST API routes.


bcrypt

const bcrypt = require('bcrypt')

bcrypt is used to hash passwords and compare passwords against stored hashes.

A password should never be stored as plain text.


path

const path = require('path')

The path module helps construct file paths safely.


SQLite

const {open} = require('sqlite')
const sqlite3 = require('sqlite3')

These packages are used to connect the Node.js application to a SQLite database.


jsonwebtoken

const jwt = require('jsonwebtoken')

The jsonwebtoken package is responsible for creating and verifying JWTs.


Creating the Express Application

We begin by creating the Express application:

const app = express()

app.use(express.json())

The following middleware:

app.use(express.json())

allows Express to read JSON request bodies.

For example, when a client sends:

{
  "username": "john",
  "password": "secret"
}

Express makes the data available through:

request.body

Connecting to the SQLite Database

The database path can be created with:

const dbPath = path.join(__dirname, 'covid19IndiaPortal.db')

We can then initialize the database:

let db = null

const initializeDatabase = async () => {
  try {
    db = await open({
      filename: dbPath,
      driver: sqlite3.Database,
    })

    app.listen(3000, () => {
      console.log('Server Started at http://localhost:3000')
    })
  } catch (error) {
    console.log(`DB Error: ${error.message}`)
    process.exit(1)
  }
}

initializeDatabase()

The server starts only after the database connection has been successfully established.


The Most Important Part: JWT Middleware

The core of JWT authentication is middleware.

In Express, middleware is a function that runs during the request-response cycle.

Our authentication middleware can be written as:

const authenticateToken = (request, response, next) => {
  const authHeader = request.headers['authorization']

  if (authHeader === undefined) {
    return response.status(401).send('Invalid JWT Token')
  }

  const jwtToken = authHeader.split(' ')[1]

  if (jwtToken === undefined) {
    return response.status(401).send('Invalid JWT Token')
  }

  jwt.verify(jwtToken, process.env.JWT_SECRET, (error, payload) => {
    if (error) {
      return response.status(401).send('Invalid JWT Token')
    }

    request.user = payload
    next()
  })
}

Let's understand it carefully.


Step 1: Read the Authorization Header

The client is expected to send:

Authorization: Bearer eyJ...

We retrieve it using:

const authHeader = request.headers['authorization']

If the header does not exist, the user has not provided a token.

We return:

return response.status(401).send('Invalid JWT Token')

Step 2: Extract the JWT

The header normally has this format:

Bearer TOKEN

Using:

authHeader.split(' ')

we get two parts:

Bearer
TOKEN

Therefore:

const jwtToken = authHeader.split(' ')[1]

extracts the actual token.

A more defensive implementation can also verify that the scheme is actually Bearer.


Step 3: Verify the Token

The token is verified with:

jwt.verify(jwtToken, process.env.JWT_SECRET, ...)

The secret should come from an environment variable rather than being hard-coded into source code.

For example:

JWT_SECRET=your-long-random-secret

If the token has been altered or cannot be verified, the request is rejected.


Step 4: Continue the Request

If the token is valid:

next()

is called.

next() tells Express:

Authentication succeeded. Continue to the next middleware or route handler.

This is what allows us to protect individual API endpoints.


Why Middleware Is Useful

Instead of repeating JWT verification in every API:

app.get('/states', ...)
app.get('/districts', ...)
app.delete('/districts/:districtId', ...)

we can attach the authentication middleware:

app.get('/states', authenticateToken, ...)

Now /states requires a valid JWT.

This makes the authentication logic reusable.


User Registration API

The registration endpoint is:

app.post('/users/', async (request, response) => {
  const {
    username,
    name,
    password,
    gender,
    location
  } = request.body

  const hashedPassword = await bcrypt.hash(password, 10)

  // Check whether username already exists
  // Insert user if it does not exist
})

The password is hashed using:

bcrypt.hash(password, 10)

The number 10 represents the bcrypt cost factor used by this example.

The important principle is:

Plain password
      ↓
bcrypt
      ↓
Password hash
      ↓
Database

The original password should not be stored directly.


Why Password Hashing Matters

Suppose a database contains:

username: john
password: mypassword123

Storing passwords like this is dangerous.

Instead, the database should contain a password hash such as:

username: john
password: $2b$10$...

During login, bcrypt compares the supplied password against the stored hash.


Login API

The login endpoint is:

app.post('/login', async (request, response) => {
  const {username, password} = request.body

  // Find user
  // Compare password
  // Generate JWT
})

First, we locate the user.

Then:

const isPasswordMatched =
  await bcrypt.compare(password, dbUser.password)

checks whether the supplied password matches the stored bcrypt hash.

If the password is correct, we create a JWT.


Creating the JWT

The token payload can contain a user identifier:

const payload = {
  username: username
}

Then:

const jwtToken = jwt.sign(
  payload,
  process.env.JWT_SECRET,
  {
    expiresIn: '30m'
  }
)

The token can then be returned:

response.send({
  jwtToken
})

Using an expiration time is important because JWTs should generally not remain valid indefinitely.


Why Should a JWT Expire?

Imagine a token is stolen.

If the token never expires, it may remain usable until it is otherwise revoked or the signing credentials are changed.

An expiration time limits how long the token remains valid.

For example:

expiresIn: '30m'

means the token expires after 30 minutes.

The appropriate lifetime depends on the application's security requirements.


Protected API: Get All States

The states endpoint can be protected like this:

app.get('/states', authenticateToken, async (request, response) => {
  const getStateDetails = `
    SELECT *
    FROM state
  `

  const stateList = await db.all(getStateDetails)

  response.send(stateList)
})

Notice:

authenticateToken

appears before the route handler.

The request flow is:

GET /states
     |
     v
authenticateToken
     |
     +---- Invalid token → 401
     |
     +---- Valid token
              |
              v
         Route handler
              |
              v
         Database query
              |
              v
            Response


Protected API: Get a Single State

We can also protect a route that receives a parameter:

app.get(
  '/states/:stateId/',
  authenticateToken,
  async (request, response) => {
    const {stateId} = request.params

    // Query the database
  }
)

Here:

/states/5

means:

stateId = 5

The authentication middleware executes before the database operation.


Protected API: Add a District

The POST endpoint can also require authentication:

app.post(
  '/districts/',
  authenticateToken,
  async (request, response) => {
    const {
      districtName,
      stateId,
      cases,
      cured,
      active,
      deaths
    } = request.body

    // Insert district
  }
)

This prevents an unauthenticated request from directly reaching the database operation.


Protected API: Get District Details

A GET endpoint can use the same middleware:

app.get(
  '/districts/:districtId/',
  authenticateToken,
  async (request, response) => {
    const {districtId} = request.params

    // Fetch district information
  }
)

The same JWT authentication logic can therefore protect multiple endpoints.


Protected API: Delete a District

Deleting information is another operation that should generally require authentication:

app.delete(
  '/districts/:districtId/',
  authenticateToken,
  async (request, response) => {
    const {districtId} = request.params

    // Delete district
  }
)

The request must include a valid JWT before the deletion logic is executed.


Protected API: Update a District

Similarly, an update operation can be protected:

app.put(
  '/districts/:districtId/',
  authenticateToken,
  async (request, response) => {
    const {districtId} = request.params

    // Update district
  }
)

This demonstrates an important advantage of middleware: the same authentication mechanism can be reused for GET, POST, PUT, and DELETE APIs.


State Statistics API

The application can also expose a protected endpoint for aggregated statistics:

app.get(
  '/states/:stateId/stats/',
  authenticateToken,
  async (request, response) => {
    const {stateId} = request.params

    // Calculate state statistics
  }
)

The authentication process remains exactly the same.

Only the business logic changes.


Complete Authentication Architecture

The overall application can be visualized as:

                 Client
                   |
                   |
          Username + Password
                   |
                   v
              /login
                   |
                   v
          Verify bcrypt password
                   |
                   v
              Generate JWT
                   |
                   v
              Return Token
                   |
                   |
       Authorization: Bearer JWT
                   |
                   v
        +----------------------+
        | authenticateToken    |
        +----------------------+
             /          \
            /            \
       Invalid          Valid
          |               |
          v               v
       401 Error      API Handler
                          |
                          v
                       Database

This separation makes the application easier to understand and maintain.


Important Security Improvements

The original sample is useful for learning JWT concepts, but there are several things that should be improved before using similar code in a production application.

1. Do Not Hard-Code the JWT Secret

Avoid:

jwt.sign(payload, 'MY_SECRET_TOKEN')

and:

jwt.verify(token, 'MY_SECRET_TOKEN')

Instead, use an environment variable:

const JWT_SECRET = process.env.JWT_SECRET

Then configure the secret outside the source code.


2. Add Token Expiration

Instead of generating a token with no explicit expiration:

jwt.sign(payload, JWT_SECRET)

use an expiration policy appropriate for the application:

jwt.sign(payload, JWT_SECRET, {
  expiresIn: '30m'
})

The exact duration should be chosen according to the application's security and usability requirements.


3. Use Parameterized SQL Queries

The original code builds SQL queries using string interpolation, for example:

const query =
  `SELECT * FROM user WHERE username = '${username}'`

This can expose the application to SQL injection.

A safer approach is parameterized SQL:

const query = `
  SELECT *
  FROM user
  WHERE username = ?
`

const dbUser = await db.get(query, username)

The same principle should be applied to INSERT, UPDATE, DELETE, and other queries.


4. Validate Request Data

Authentication does not replace input validation.

The server should validate:

  • Username
  • Password
  • State ID
  • District ID
  • Numeric fields
  • Required properties
  • Allowed value ranges

For example, an API should not blindly trust:

request.body

5. Do Not Put Sensitive Data in the JWT

Avoid putting information such as:

password
credit card number
private API key
database password

inside the JWT payload.

JWT payloads should contain only the claims required by the application.


6. Authentication Is Not Authorization

This is an important distinction.

Authentication

Answers:

Who are you?

JWT verification can establish that the token is valid and associated with a user.

Authorization

Answers:

What are you allowed to do?

For example:

Normal user → Can read data
Admin       → Can create, update, and delete data

A production application may therefore need both JWT authentication and role/permission checks.


Common JWT Use Cases

JWT authentication can be useful in many types of applications.

REST APIs

JWTs are commonly used to protect API endpoints.

Example:

POST /login
GET /profile
GET /orders
POST /orders

Single-Page Applications

Applications built with frontend frameworks can authenticate with a backend API and use access secret management, password hashing, token expiration, input validation, parameterized SQL queries, HTTPS, and an appropriate token-storage and revocation strategy tokens when making API requests.


Mobile Applications

Mobile applications can also authenticate against an API and present an access token when accessing protected resources.


Microservices

JWTs can be useful for passing authenticated identity information between services, although the exact architecture depends on the system's security model.


JWT vs Session-Based Authentication

JWT authentication and traditional server-side sessions solve related problems but use different approaches.

Session Authentication

The server stores session information.

Client → Session ID → Server Session Store

JWT Authentication

The client presents a signed token containing claims.

Client → JWT → Server Verification

Neither approach is automatically the right choice for every application. The architecture, security requirements, token lifetime, revocation strategy, and deployment model should determine the choice.


Example Request With JWT

After login, suppose the server returns:

{
  "jwtToken": "eyJ..."
}

A subsequent request might contain:

GET /states
Authorization: Bearer eyJ...

The server extracts the token:

const authHeader = request.headers['authorization']
const token = authHeader.split(' ')[1]

and verifies it:

jwt.verify(token, JWT_SECRET, callback)

If verification succeeds, the request reaches the protected endpoint.


Simplified JWT Middleware

For learning purposes, the core middleware can be summarized as:

const authenticateToken = (request, response, next) => {
  const authHeader = request.headers['authorization']

  if (!authHeader) {
    return response.status(401).send('Unauthorized')
  }

  const token = authHeader.split(' ')[1]

  jwt.verify(token, process.env.JWT_SECRET, (error, payload) => {
    if (error) {
      return response.status(401).send('Invalid JWT Token')
    }

    request.user = payload
    next()
  })
}

The three most important operations are:

Read token
    ↓
Verify token
    ↓
Allow or reject request


Complete JWT Authentication Flow

The complete process can be remembered using this sequence:

1. User registers
       ↓
2. Password is hashed
       ↓
3. User logs in
       ↓
4. Password hash is verified
       ↓
5. JWT is generated
       ↓
6. Client receives token
       ↓
7. Client sends Bearer token
       ↓
8. Middleware verifies JWT
       ↓
9. Valid token → API executes
       ↓
10. Invalid token → 401 response

Key Points to Remember

If you are learning JWT authentication for the first time, remember these concepts:

  • JWT means JSON Web Token.
  • JWTs can be used to authenticate API requests.
  • Passwords should be hashed before being stored.
  • bcrypt can be used for password hashing and verification.
  • jsonwebtoken can create and verify JWTs in Node.js.
  • The token is commonly sent through the Authorization header.
  • The common format is Bearer <token>.
  • Express middleware can protect multiple API routes.
  • JWT payloads should not contain sensitive secrets.
  • JWT secrets should be stored securely, such as in environment variables.
  • Tokens should generally have an appropriate expiration time.
  • Authentication and authorization are different concepts.
  • SQL queries should use parameterized values rather than string interpolation.


Conclusion

JWT authentication provides a practical way to protect Node.js and Express.js APIs using signed tokens.
The basic concept is straightforward:
Login
↓
Verify Password
↓
Generate JWT
↓
Client Sends JWT
↓
Middleware Verifies JWT
↓
Access Protected API
The most important part of the implementation is the authentication middleware. Instead of writing token verification separately for every endpoint, Express allows us to create one reusable middleware function and attach it to all routes that require authentication.
The sample application demonstrates this pattern across state and district APIs, including GET, POST, PUT, and DELETE operations.
For production systems, however, JWT should be implemented alongside secure secret management, password hashing, token expiration, input validation, parameterized SQL queries, HTTPS, and an appropriate token-storage and revocation strategy.

SEO Keywords

JWT authentication Node.js, JWT authentication Express.js, JSON Web Token JavaScript, JWT login example Node.js, Express JWT middleware, Node.js JWT authentication example, JWT token verification, bcrypt password hashing Node.js, REST API JWT authentication, JWT protected routes, Express authentication middleware, Node.js authentication tutorial.