Guide on how to Implement authorization and authentication in an Express.js API with Passport.js

Guide on how to Implement authorization and authentication in an Express.js API with Passport.js

Detailed guide on how to implement authorization and authentication, including email verification, in an Express.js API with Passport.js

Install the necessary packages:

To get started, you will need to install the following packages:

  • express: This is the web framework for building the API.

  • passport: This is an authentication middleware for Node.js that helps you authenticate requests.

  • passport-local: This is a Passport strategy for authenticating with a username and password.

  • mongoose: This is a MongoDB object modeling tool that helps you interact with the database.

  • bcrypt: This is a password hashing function that helps you securely store and compare passwords.

  • jsonwebtoken: This is a library for generating and verifying JSON web tokens (JWTs) which will be used to authenticate users.

  • nodemailer: This is a library for sending emails, which will be used to send the email verification link.

You can install these packages using the following command:

npm install express passport passport-local mongoose bcrypt jsonwebtoken nodemailer

Set up the database:

Next, you will need to set up a MongoDB database to store the user data. You can use a service like MongoDB Atlas to set up a cloud-hosted database, or you can install and run MongoDB locally.

Once you have set up the database, create a user model in your Express.js API that represents a user in the database. The model should include fields for the user's email, password, and role. The role field could be a string that specifies whether the user is an admin, project manager, or user. You can also include a field for the user's email verification status.

Here is an example of what the user model might look like:

const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  },
  role: {
    type: String,
    required: true,
    default: 'user'
  },
  emailVerified: {
    type: Boolean,
    default: false
  }
});

userSchema.pre('save', async function(next) {
  try {
    if (!this.isModified('password')) {
      return next();
    }

    const hashedPassword = await bcrypt.hash(this.password, 10);
    this.password = hashedPassword;
    next();
  } catch (err) {
    next(err);
  }
});

userSchema.methods.comparePassword = async function(candidatePassword, next) {
  try {
    const isMatch = await bcrypt.compare(candidatePassword, this.password);
    return isMatch;
  } catch (err) {
    return next(err);
  }
};

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

module.exports = User;

Set up Passport.js:

Next, you will need to set up Passport.js to handle the authentication process. To do this, you will need to create a Passport strategy for authenticating with a username and password.

First, create a file called passport.js and require the necessary packages:

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('./models/user');

Then, define the Passport strategy:

passport.use(
  new LocalStrategy(
    {
      usernameField: 'email',
      passwordField: 'password'
    },
    async (email, password, done) => {
      try {
        // Find the user with the given email
        const user = await User.findOne({ email });

        // If the user is not found, return a message
        if (!user) {
          return done(null, false, { message: 'Email not found' });
        }

        // Check if the password is correct
        const isMatch = await user.comparePassword(password);

        // If the password is incorrect, return a message
        if (!isMatch) {
          return done(null, false, { message: 'Password is incorrect' });
        }

        // If the email and password are correct, return the user object
        return done(null, user);
      } catch (err) {
        done(err);
      }
    }
  )
);

Then, configure Passport to serialize and deserialize users:

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findById(id);
    done(null, user);
  } catch (err) {
    done(err, null);
  }
});

Set up email verification:

To implement email verification, you will need to generate and send an email verification link to the user when they register. The link should contain a unique token that can be used to verify the user's email address.

To generate the token, you can use the jsonwebtoken library. Here is an example of how to generate and send the email verification link:

const jwt = require('jsonwebtoken');
const nodemailer = require('nodemailer');

const sendVerificationEmail = async (user) => {
    try {
        // Generate the verification token
        const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, {
            expiresIn: '1d'
        });

        // Set up the email transporter
        const transporter = nodemailer.createTransport({
            service: 'gmail',
            auth: {
                user: process.env.EMAIL_USERNAME,
                pass: process.env.EMAIL_PASSWORD
            }
        });

        // Define the email options
        const mailOptions = {
            from: process.env.EMAIL_USERNAME,
            to: user.email,
            subject: 'Email verification',
            html: `
        <p>Please click the following link to verify your email address:</p>
            <p>
                <a href="http://localhost:3000/verify-email/${token}">
                    http://localhost:3000/verify-email/${token}
                </a>
            </p>`
        };
        // Send the email
        await transporter.sendMail(mailOptions);
    } catch (err) {
        throw new Error(err);
    }
};

To verify the email address, you will need to create a route in your Express.js API that verifies the token and marks the user's email as verified in the database. Here is an example of how to do this:

const jwt = require('jsonwebtoken');

router.get('/verify-email/:token', async (req, res) => {
    try {
        // Verify the token
        const decoded = jwt.verify(req.params.token, process.env.JWT_SECRET);
        // Update the user's emailVerified field
        const user = await User.findByIdAndUpdate(
            decoded.userId,
            { emailVerified: true },
            { new: true }
        );

        // If the user is not found, return an error message
        if (!user) {
            return res.status(404).send({ message: 'User not found' });
        }

        // If the user is found, return a success message
        res.send({ message: 'Email verified successfully' });
    } catch (err) {
        // If the token is invalid, return an error message
        res.status(400).send({ message: 'Invalid token' });
    }
});

Protect routes with middleware:

To implement authorization and protect certain routes from unauthorized access, you can use middleware to check the user's role and email verification status. First, create a middleware function that checks if the user is logged in:

const checkAuth = (req, res, next) => {
    if (req.isAuthenticated()) {
        return next();
    }

    return res.status(401).send({ message: 'Unauthorized' });
};

Then, create another middleware function that checks if the user has a specific role:

const checkRole = (roles) => (req, res, next) => {
    if (roles.includes(req.user.role)) {
        return next();
    }

    return res.status(401).send({ message: 'Unauthorized' });
};

Finally, use the middleware functions to protect specific routes. For example, to protect the /admin route and allow only admins to access it, you can use the following code:

router.get('/admin', checkAuth, checkRole(['admin']), (req, res) => {
    res.send({ message: 'Welcome to the admin area' });
});

You can also use the checkAuth middleware function to require email verification before allowing access to certain routes. For example, to protect the /profile route and require email verification before allowing access, you can use the following code:

router.get('/profile', checkAuth, async (req, res) => {
    try {
        const user = await User.findById(req.user.id);
        // If the user's email is not verified, return an error message
        if (!user.emailVerified) {
            return res.status(401).send({ message: 'Email not verified' });
        }

        // If the user's email is verified, return the user's profile
        res.send({ message: 'Welcome to your profile', user });
    } catch (err) {
        res.status(500).send(err);
    }
});

Set up login and registration routes:

To set up the login and registration routes, you will need to use Passport.js's authenticate function and the checkAuth and checkRole middleware functions. First, create the login route:

router.post(
    '/login',
    passport.authenticate('local', { session: false }),
    (req, res) => {
        // Generate a JWT and send it to the client
        const token = jwt.sign({ userId: req.user.id }, process.env.JWT_SECRET, {
            expiresIn: '7d'
        });
        res.send({ token });
    }
);

Then, create the registration route:

router.post('/register', async (req, res) => {
    try {
        // Create a new user
        const user = new User({
            email: req.body.email,
            password: req.body.password,
            role: req.body.role
        });
        // Save the user to the database
        await user.save();

        // Send the email verification link
        await sendVerificationEmail(user);

        // Return a success message
        res.send({ message: 'Registration successful, please check your email' });
    } catch (err) {
        res.status(500).send(err);
    }
});

That's it! You now have a fully functional system for implementing authorization and authentication, including email verification, in an Express.js API with Passport.js.

Did you find this article valuable?

Support Shreyash Rangrej by becoming a sponsor. Any amount is appreciated!