Auth with Node.js, JWT, and MongoDB

Introduction

Node.js is a popular JavaScript runtime that allows you to build server-side applications using JavaScript. One common use case for Node.js is building web applications, and a key component of many web applications is user authentication.

MongoDB is a popular NoSQL database that is often used with Node.js applications. MongoDB allows you to store data in a flexible, document-based format that can be easily queried using the MongoDB Query Language (MQL).

When building a Node.js authentication system with MongoDB, you typically create a User model using a MongoDB Object Document Mapper (ODM) like Mongoose. This User model represents a user in your authentication system and contains properties like email, password, and name.

To authenticate a user, you typically compare the user's inputted password to a hashed password that is stored in the database. You can use a library like bcrypt to hash passwords and compare them to inputted passwords.

To manage authentication sessions, you can use a technique called JSON Web Tokens (JWT). JWTs allow you to generate a token that contains information about the user (such as their user ID), which can be securely transmitted to the client and used to authenticate subsequent requests.

Step by step

Now we are going to see how to do it step by step:

Step 1: Set up your project

Create a new project directory and navigate into it using your terminal. Then run the following command to create a package.json file:

1 npm init -y

This will initialize a new Node.js project with default settings.

Step 2: Install dependencies

We need to install the following dependencies:

1 npm install express mongoose body-parser jsonwebtoken bcrypt dotenv

Here's a brief description of each dependency:

- Express: A web framework for Node.js

- Mongoose: A MongoDB object modeling tool

- Body-parser: Middleware for handling JSON data

- Jsonwebtoken: A library for creating and verifying JSON web tokens (JWT)

- Bcrypt: A library for hashing and salting passwords

- Dotenv: A library for managing environment variables

Step 3: Create a MongoDB database

Before creating our authentication system, we need to set up a MongoDB database. You can use a local instance of MongoDB or sign up for a free MongoDB Atlas account.

Step 4: Create a .env file

Create a .env file in the root directory of your project and add the following variables:

1 2 3 PORT=3000 MONGODB_URI=<your-mongodb-uri> JWT_SECRET=<your-jwt-secret>

Make sure to replace <your-mongodb-uri> and <your-jwt-secret> with your own values.

Step 5: Create our server.js, the entry point of our app

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // Import dependencies and app code const express = require('express'); const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); // Create a new Express app const app = express(); // Set up middleware app.use(express.json()); // Connect to MongoDB mongoose.connect('mongodb://localhost/auth-demo', { useNewUrlParser: true, useUnifiedTopology: true }) .then(() => console.log('Connected to MongoDB...')) .catch(err => console.error('Could not connect to MongoDB...', err)); // Start the server const port = process.env.PORT || 3000; app.listen(port, () => console.log(`Listening on port ${port}...`));

Step 6: Create a user model

Create a new file called user.js in a new folder called models. This will define our user schema and model.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); const userSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true, lowercase: true, trim: true, }, password: { type: String, required: true, minlength: 6, }, }); userSchema.pre('save', async function(next) { try { // Generate a salt const salt = await bcrypt.genSalt(10); // Generate a password hash (salt + hash) const passwordHash = await bcrypt.hash(this.password, salt); // Re-assign hashed version over original, plain text password this.password = passwordHash; next(); } catch (error) { next(error); } }); userSchema.methods.isValidPassword = async function(newPassword) { try { return await bcrypt.compare(newPassword, this.password); } catch (error) { throw new Error(error); } }; const User = mongoose.model('user', userSchema); module.exports = User;

This model defines an email and password field for our users. We're also using the bcrypt library to hash and salt passwords.

Step 7: Create an authentication route

Create a new file called auth.js in a new folder called routes. This will define our authentication route.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 const express = require('express'); const router = express.Router(); const User = require('../models/user'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); router.post('/register', async (req, res) => { const { name, email, password } = req.body; try { // Check if user already exists const existingUser = await User.findOne({ email }); if (existingUser) { return res.status(400).json({ error: 'User already exists' }); } // Create a new user const salt = await bcrypt.genSalt(); const hashedPassword = await bcrypt.hash(password, salt); const newUser = new User({ name, email, password: hashedPassword }); const savedUser = await newUser.save(); // Create and sign a JWT token const token = jwt.sign({ userId: savedUser._id }, process.env.JWT_SECRET); // Return the token and user information res.json({ token, user: { id: savedUser._id, name: savedUser.name, email: savedUser.email } }); } catch (error) { console.error(error); res.status(500).json({ error: 'Server error' }); } }); module.exports = router;

Step 8: create a login route to verify the jwt token:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 router.post('/login', async (req, res, next) => { const { email, password } = req.body; // Find the user by email const user = await User.findOne({ email }); if (!user) { return res.status(422).json({ error: 'Invalid email or password' }); } // Check if the password is valid const isValidPassword = await user.isValidPassword(password); if (!isValidPassword) { return res.status(422).json({ error: 'Invalid email or password' }); } // Generate a JWT token const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET); // Respond with the token res.json({ token }); });

This route handles the POST request to /login. It checks if the user with the given email exists, and then checks if the given password is valid. If the user is authenticated, it generates a JWT token and sends it back to the client.

Step 9: Protect routes with authentication middleware

Now that we have a way to authenticate users, we need to protect routes that require authentication. We'll create a middleware function that checks if the request contains a valid JWT token:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const requireAuth = (req, res, next) => { const token = req.headers.authorization; if (!token) { return res.status(401).json({ error: 'Unauthorized' }); } jwt.verify(token, process.env.JWT_SECRET, async (err, payload) => { if (err) { return res.status(401).json({ error: 'Unauthorized' }); } const { userId } = payload; const user = await User.findById(userId); req.user = user; next(); }); }; module.exports = requireAuth;

This middleware function checks if the request contains a Authorization header with a valid JWT token. If the token is valid, it extracts the userId from the payload and fetches the corresponding user from the database. Finally, it attaches the user object to the request and calls the next function to continue processing the request.

Step 10: Use the authentication middleware

We can now protect routes that require authentication by using the requireAuth middleware function. For example, let's create a protected route that returns the user's email:

1 2 3 4 5 6 7 8 9 10 const express = require('express'); const requireAuth = require('../middleware/requireAuth'); const router = express.Router(); router.get('/profile', requireAuth, async (req, res, next) => { res.json({ email: req.user.email }); }); module.exports = router;

This route is protected by the requireAuth middleware, which means the user must provide a valid JWT token in the Authorization header to access it. If the user is authenticated, the route returns the user's email.

That's it! You now have a basic authentication system using Node.js and JWT. Of course, this is just a starting point, and you'll likely want to add more features and security measures to your system.

it was a pleasure helping you with your authentication system using Node.js and JWT! Keep in mind that this is just one way to implement authentication in your application, and there are many other options out there. Make sure to always follow best practices and keep security in mind when dealing with user authentication. If you have any questions or need further assistance, don't hesitate to ask! Good luck with your project!

More Posts
Created by Roberto Espinoza