Skip to content

Implementing JWT Authentication in Node.js with Mongoose

JWT (JSON Web Token) authentication is a secure, stateless method for handling user authentication in web applications. Unlike traditional sessions stored on the server, JWTs allow you to manage authentication directly in the client, making your application more scalable. In this guide, we’ll walk through implementing JWT authentication in a Node.js application using Express and Mongoose, covering everything from token generation to securing routes.


What is JWT Authentication?

JWT is a secure, stateless token format commonly used in REST APIs for authentication. A JWT is a string with three parts: header, payload, and signature. When a user logs in, the server generates a token and sends it to the client. This token is then included in each request to validate the user’s identity.

Structure of a JWT

A JWT looks like this:

header.payload.signature
  • Header: Contains metadata, including the type (JWT) and hashing algorithm.
  • Payload: Holds the data (claims) about the user, such as user ID, role, and expiration.
  • Signature: Verifies the authenticity of the token using a secret key.
graph LR
    subgraph "JWT Structure"
        Header[Header<br/>Type: JWT<br/>Algorithm: HS256]
        Payload[Payload<br/>User ID: 123<br/>Username: john<br/>Exp: 1640995200]
        Signature[Signature<br/>HMACSHA256<br/>secret key]
    end
    
    Header --> Dot1[.]
    Dot1 --> Payload
    Payload --> Dot2[.]
    Dot2 --> Signature
    
    subgraph "Encoded JWT"
        Token[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<br/>eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.<br/>SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c]
    end
    
    Signature --> Token
    
    style Header fill:#e1f5fe
    style Payload fill:#f3e5f5
    style Signature fill:#fff3e0
    style Token fill:#e8f5e8

Prerequisites

To follow along, you’ll need:

  1. Node.js and npm installed.
  2. MongoDB and Mongoose for managing user data.
  3. Basic knowledge of Node.js, Express, and Mongoose.

Setting Up the Project

Let’s start by setting up a new Node.js project and installing the necessary dependencies.

Step 1: Initialize the Project

# @filename: script.sh
mkdir jwt-auth
cd jwt-auth
npm init -y

Step 2: Install Dependencies

Install Express, Mongoose, jsonwebtoken, and bcryptjs.

npm install express mongoose jsonwebtoken bcryptjs dotenv
  • express: For building the server.
  • mongoose: To interact with MongoDB.
  • jsonwebtoken: To create and verify JWTs.
  • bcryptjs: To hash passwords securely.
  • dotenv: For managing environment variables.

Step 3: Set Up Environment Variables

Create a .env file in the project root with your MongoDB URI and JWT secret.

// @filename: .env
MONGODB_URI=mongodb://localhost:27017/jwt_auth
JWT_SECRET=your_jwt_secret
PORT=5000

Defining the User Model

Create a models folder and define a User model with Mongoose. The model includes fields for username, email, and hashed password.

models/User.js

// @filename: config.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')

const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
})

// Hash password before saving user
userSchema.pre('save', async function (next) {
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 10)
  }
  next()
})

// Compare password for login
userSchema.methods.comparePassword = async function (password) {
  return bcrypt.compare(password, this.password)
}

module.exports = mongoose.model('User', userSchema)

This schema includes:

  • Pre-save middleware to hash the password before saving it.
  • A method comparePassword to check if a given password matches the hashed password.

Setting Up JWT Authentication

Let’s create functions for registering users, logging in, and generating a JWT.

JWT Authentication Flow

sequenceDiagram
    participant Client
    participant Server
    participant DB as Database
    
    Note over Client,DB: Registration Process
    Client->>Server: POST /auth/register<br/>{username, email, password}
    Server->>DB: Check if user exists
    DB-->>Server: User not found
    Server->>DB: Hash password & save user
    DB-->>Server: User created successfully
    Server->>Server: Generate JWT token
    Server-->>Client: {user, token}
    
    Note over Client,DB: Login Process
    Client->>Server: POST /auth/login<br/>{email, password}
    Server->>DB: Find user by email
    DB-->>Server: User found
    Server->>Server: Compare password hash
    Server->>Server: Generate JWT token
    Server-->>Client: {user, token}
    
    Note over Client,DB: Protected Route Access
    Client->>Server: GET /profile<br/>Authorization: Bearer <token>
    Server->>Server: Verify JWT token
    Server->>DB: Get user by ID from token
    DB-->>Server: User data (without password)
    Server-->>Client: User profile data

Step 1: Setting Up Express and Connecting to MongoDB

Create a server.js file and initialize the Express app, configure middleware, and connect to MongoDB.

server.js

// @filename: server.js
require('dotenv').config()
const express = require('express')
const mongoose = require('mongoose')

const app = express()
app.use(express.json())

// Connect to MongoDB
mongoose
  .connect(process.env.MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => console.log('Connected to MongoDB'))
  .catch((error) => console.error('MongoDB connection error:', error))

// Routes
app.use('/auth', require('./routes/auth'))

const port = process.env.PORT || 3000
app.listen(port, () => {
  console.log(`Server running on port ${port}`)
})

Step 2: Creating the Auth Routes

In the routes folder, create an auth.js file for handling authentication routes: register and login.

routes/auth.js

// @filename: routes.js
const express = require('express')
const jwt = require('jsonwebtoken')
const User = require('../models/User')

const router = express.Router()

// Helper function to generate JWT
const generateToken = (user) => {
  return jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' })
}

// Register Route
router.post('/register', async (req, res) => {
  try {
    const { username, email, password } = req.body
    const existingUser = await User.findOne({ email })
    if (existingUser)
      return res.status(400).json({ message: 'Email already registered' })

    const user = new User({ username, email, password })
    await user.save()

    const token = generateToken(user)
    res.status(201).json({ user, token })
  } catch (error) {
    res.status(500).json({ message: error.message })
  }
})

// Login Route
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body
    const user = await User.findOne({ email })
    if (!user) return res.status(404).json({ message: 'User not found' })

    const isPasswordValid = await user.comparePassword(password)
    if (!isPasswordValid)
      return res.status(400).json({ message: 'Invalid credentials' })

    const token = generateToken(user)
    res.status(200).json({ user, token })
  } catch (error) {
    res.status(500).json({ message: error.message })
  }
})

module.exports = router

In this code:

  • Register Route: Creates a new user, hashes their password, generates a JWT, and returns it with the user data.
  • Login Route: Verifies the user’s credentials, generates a JWT if successful, and returns it.

Token Generation

The generateToken function uses jwt.sign to create a token with a payload containing the user’s id and a 1-hour expiration.


Protecting Routes with JWT Middleware

Now, let’s create middleware to protect routes by verifying the JWT token.

Step 1: Creating the Authentication Middleware

In the middleware folder, create authMiddleware.js to check if the user is authenticated.

middleware/authMiddleware.js

// @filename: config.js
const jwt = require('jsonwebtoken')

const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1]
  if (!token)
    return res
      .status(401)
      .json({ message: 'Access denied. No token provided.' })

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    req.user = decoded // Attach the decoded user id to request
    next()
  } catch (error) {
    res.status(400).json({ message: 'Invalid token.' })
  }
}

module.exports = authMiddleware

This middleware:

  1. Extracts the token from the Authorization header.
  2. Verifies the token using the JWT_SECRET.
  3. Attaches the decoded user ID to req.user if the token is valid.

Step 2: Protecting a Route

You can now use authMiddleware to protect any route. For example, create a profile route that only authenticated users can access.

routes/profile.js

// @filename: routes.js
const express = require('express')
const authMiddleware = require('../middleware/authMiddleware')
const User = require('../models/User')

const router = express.Router()

router.get('/profile', authMiddleware, async (req, res) => {
  try {
    const user = await User.findById(req.user.id).select('-password') // Exclude password
    res.status(200).json(user)
  } catch (error) {
    res.status(500).json({ message: error.message })
  }
})

module.exports = router

In server.js, add the profile route:

app.use('/profile', require('./routes/profile'))

Now, only authenticated users with a valid token can access the /profile route.


Testing the API

Use a tool like Postman to test your API:

  1. Register a new user by sending a POST request to /auth/register.

  2. Login with the user credentials by sending a POST request to /auth/login and get a token.

  3. Access the Profile Route by sending a GET request to /profile with the token in the Authorization header:

    Authorization: Bearer <token>

Best Practices

for JWT Authentication

  1. Use HTTPS: Always use HTTPS to prevent token interception.
  2. Set Expiration: Set token expiration to reduce the risk of token misuse if it’s compromised.
  3. Store Tokens Securely: Store tokens in secure storage (e.g., HTTP-only cookies or secure client storage).
  4. Implement Refresh Tokens: Use refresh tokens for session management and renew tokens upon expiration.
  5. Validate Token Expiry: Handle expired tokens gracefully on the client side to prompt users to log in again.

Conclusion

Implementing JWT authentication in a Node.js application with Mongoose provides a secure, stateless approach to managing user sessions. By creating a token during login and attaching it to requests, you can authenticate users and protect routes with ease.

With JWT authentication, your application is scalable, secure, and able to handle authenticated requests without relying on server-side sessions. Integrate these techniques into your project to enhance your application’s security and improve the user experience with seamless authentication.

Node.js JavaScript Backend Express REST API JWT
Share:

Continue Reading

Implementing Authentication in a RESTful API with Node.js, Express, and JWT

Authentication is a fundamental aspect of secure API development, allowing you to protect routes and control access to resources. By implementing JWT (JSON Web Tokens) for authentication in a Node.js and Express application, you can achieve stateless and secure access for your users. This guide covers setting up user registration, logging in, generating JWT tokens, and securing routes with JWT-based authentication.

Read article
Node.jsJavaScriptBackend

Implementing JWT Refresh Tokens in Node.js for Secure User Authentication

While JWTs (JSON Web Tokens) are a secure way to handle authentication, they are often short-lived, with a limited expiration time to reduce security risks if compromised. To maintain user sessions without requiring frequent re-authentication, refresh tokens allow you to renew access tokens safely. This guide walks through implementing refresh tokens in a Node.js application using Express and Mongoose, allowing for secure and scalable session management.

Read article
Node.jsJavaScriptBackend

Implementing Password Reset Functionality in Node.js with JWT and Nodemailer

Password reset functionality is essential for any application that requires user authentication. By combining JWT (JSON Web Token) with Nodemailer, you can build a secure password reset process that allows users to reset their passwords without exposing sensitive data. This guide will walk you through implementing a password reset feature in Node.js with Express, Mongoose, and Nodemailer.

Read article
Node.jsJavaScriptBackend