Authentication

Introduction

Authentication is a crucial part of web application security. It ensures that users are who they say they are, protecting both user data and system integrity. In this blog, we'll dive deep into implementing authentication in a Node.js application using the Express.js framework. We'll cover everything from setting up the project to implementing secure login and registration systems.

Table of Contents

  1. Setting Up the Project

  2. Creating the User Model

  3. Setting Up Authentication Middleware

  4. Implementing User Registration

  5. Implementing User Login

  6. Protecting Routes

  7. Using JSON Web Tokens (JWT)

  8. Logout and Token Invalidation

  9. Best Practices and Security Tips

Setting Up the Project

Step 1: Initialize the Project

First, create a new directory for your project and navigate into it. Then, initialize a new Node.js project.

mkdir auth-tutorial
cd auth-tutorial
npm init -y

Step 2: Install Dependencies

Install the necessary dependencies, including Express, Mongoose (for MongoDB), bcrypt (for hashing passwords), and jsonwebtoken (for handling JWTs).

npm install express mongoose bcrypt jsonwebtoken body-parser

Creating the User Model

We'll use MongoDB to store user data, so let's set up a Mongoose model for our users.

Step 1: Connect to MongoDB

Create a file db.js to handle the database connection.

// db.js
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    await mongoose.connect('mongodb://localhost:27017/authTutorial', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log('MongoDB connected');
  } catch (error) {
    console.error(error);
    process.exit(1);
  }
};

module.exports = connectDB;

Step 2: Define the User Schema

Create a file models/User.js to define the User schema.

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

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

userSchema.pre('save', async function (next) {
  if (!this.isModified('password')) {
    return next();
  }
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

userSchema.methods.matchPassword = async function (password) {
  return await bcrypt.compare(password, this.password);
};

const User = mongoose.model('User', userSchema);

module.exports = User;

Setting Up Authentication Middleware

Step 1: Middleware for Handling JSON Web Tokens

Create a file middleware/auth.js to handle JWT verification.

// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const protect = async (req, res, next) => {
  let token;
  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith('Bearer')
  ) {
    token = req.headers.authorization.split(' ')[1];
  }

  if (!token) {
    return res.status(401).json({ message: 'Not authorized, no token' });
  }

  try {
    const decoded = jwt.verify(token, 'your_jwt_secret');
    req.user = await User.findById(decoded.id).select('-password');
    next();
  } catch (error) {
    console.error(error);
    res.status(401).json({ message: 'Not authorized, token failed' });
  }
};

module.exports = { protect };

Implementing User Registration

Step 1: Registration Route

Create a file routes/auth.js and set up the registration route.

// routes/auth.js
const express = require('express');
const User = require('../models/User');
const jwt = require('jsonwebtoken');

const router = express.Router();

router.post('/register', async (req, res) => {
  const { username, password } = req.body;

  try {
    const user = await User.create({ username, password });

    const token = jwt.sign({ id: user._id }, 'your_jwt_secret', {
      expiresIn: '30d',
    });

    res.status(201).json({ token });
  } catch (error) {
    console.error(error);
    res.status(400).json({ message: 'User registration failed' });
  }
});

module.exports = router;

Implementing User Login

Step 1: Login Route

In routes/auth.js, add a route for logging in users.

// routes/auth.js
router.post('/login', async (req, res) => {
  const { username, password } = req.body;

  try {
    const user = await User.findOne({ username });

    if (user && (await user.matchPassword(password))) {
      const token = jwt.sign({ id: user._id }, 'your_jwt_secret', {
        expiresIn: '30d',
      });
      res.json({ token });
    } else {
      res.status(401).json({ message: 'Invalid credentials' });
    }
  } catch (error) {
    console.error(error);
    res.status(400).json({ message: 'User login failed' });
  }
});

Protecting Routes

Step 1: Secure a Route

To protect a route, use the protect middleware. For example, create a protected route in routes/protected.js.

// routes/protected.js
const express = require('express');
const { protect } = require('../middleware/auth');

const router = express.Router();

router.get('/profile', protect, (req, res) => {
  res.json({
    id: req.user._id,
    username: req.user.username,
  });
});

module.exports = router;

Using JSON Web Tokens (JWT)

JWTs are used to securely transmit information between parties as a JSON object. They are compact, readable, and digitally signed, using a secret or a public/private key pair.

Step 1: Token Creation

In the registration and login routes, we use jwt.sign() to create a token.

const token = jwt.sign({ id: user._id }, 'your_jwt_secret', {
  expiresIn: '30d',
});

Step 2: Token Verification

In the protect middleware, we use jwt.verify() to verify the token.

const decoded = jwt.verify(token, 'your_jwt_secret');

Logout and Token Invalidation

Step 1: Implement Logout

While JWTs are stateless and don't require server-side invalidation, you can implement a logout by handling it client-side or maintaining a blacklist of tokens.

Best Practices and Security Tips

  1. Store Secrets Securely: Never hard-code your JWT secret in your source code. Use environment variables instead.

  2. Use HTTPS: Always use HTTPS to protect tokens from being intercepted.

  3. Short-lived Tokens: Use short-lived tokens and refresh them to minimize the risk if a token is compromised.

  4. Validate User Input: Always validate and sanitize user input to prevent injection attacks.

  5. Secure Passwords: Use bcrypt to hash passwords and never store plaintext passwords.

Conclusion

In this detailed guide, we've covered setting up a Node.js and Express.js project, creating a user model, implementing authentication middleware, and creating routes for user registration and login. By following these steps, you can implement a secure authentication system in your web application. Remember to follow best practices to keep your application and user data safe. Happy coding!

Last updated