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:
- Finds the user.
- Retrieves the stored password hash.
- Compares the supplied password with the hash.
- 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')
const express = require('express')
Express is used to create the HTTP server and REST API routes.
bcrypt
const bcrypt = require('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')
const path = require('path')
The path module helps construct file paths safely.
SQLite
const {open} = require('sqlite')
const sqlite3 = require('sqlite3')
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')
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.
bcryptcan be used for password hashing and verification.jsonwebtokencan create and verify JWTs in Node.js.- The token is commonly sent through the
Authorizationheader. - 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.
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.
0 Comments
If you have any doubts or any topics that you want to know more about them please let me know