Building a Blog API with Node.js: Authentication and Validation (Part 3)

In the previous articles in this series, we designed our API, implemented the data models and established the database connection. Before we start building the actual API, we'll cover two topics that are important for any API:

  • Authentication

  • Validation

Let's start by installing the dependencies required for this section:

npm install joi jsonwebtoken passport passport-jwt passport-local passport-local-mongoose

Authentication with Passport.js

To authenticate users in our API, we'll be using the popular Passport.js library. When a user logs in, we'll generate a JWT token for the user. The token will be used to authorise the user's requests to the API.

  1. In the /authentication/passport.js file, set up the JWT strategy:

     const passport = require("passport");
     const localStrategy = require("passport-local").Strategy;
     const { UserModel } = require("../models");
    
     const JWTstrategy = require("passport-jwt").Strategy;
     const ExtractJWT = require("passport-jwt").ExtractJwt;
    
     passport.use(
       new JWTstrategy(
         {
            secretOrKey: process.env.JWT_SECRET,
            jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken()
         },
      async (token, done) => {
         try {
             return done(null, token.user);
          } catch (error) {
             done(error);
          }
      }
      )
     );
    

    The JSON Web Token (JWT) strategy that allows us to authenticate users by verifying their JWT. To use this strategy, we pass in a secret key (which we have stored in our .env file) and a fucntion. The ExtractJWT.fromAuthHeaderAsBearerToken method will extract the JWT from the Authorization header of the request as a Bearer token. If the JWT is found, it will be decoded and passed to the function along with the done function. The function will check the user object in the JWT to see if it is valid. If it's valid, it will call the done function with the user object, indicating that the user has been authenticated, else it will call the done function with an error.

  2. For the "signup" strategy, we'll set up a local strategy which uses the passport-local module to authenticate users with a username (in this case, their email address) and password. When a user signs up, we'll create a new User document in the database and return it to Passport.

     passport.use(
         "signup",
         new localStrategy(
             {
                 usernameField: "email",
                 passwordField: "password",
                 passReqToCallback: true
             },
             async (req, email, password, done) => {
                 try {
                     const user = await UserModel.create({ 
                         ...req.body, password 
                     });
    
                     return done(null, user);
                 } catch (error) {
                     done(error);
                 }
             }
         )
       );
    
  1. The "login" strategy will also use the passport-local module. When a user logs in, we search the database for a matching email address and verify that the user exists in the database. If the login is successful, we return the user object to Passport.

     passport.use(
         "login",
         new localStrategy(
             {
                 usernameField: "email",
                 passwordField: "password",
                 passReqToCallback: true
             },
             async (req, email, password, done) => {
                 try {
                     const user = await UserModel.findOne({ email });
    
                     if (!user) {
                         return done(null, false, { 
                             message: "User not found" 
                         });
                     }
    
                     return done(null, user, { 
                         message: "Logged in Successfully" 
                     });
                 } catch (error) {
                     return done(error);
                 }
             }
         )
     );
    
  2. We still need to verify the user's password. In /src/models/user.models.js, create a method called isValidPassword() in the User model:

     UserModel.methods.isValidPassword = async function (password) {
         const user = this;
    
         const match = await bcrypt.compare(password, user.password);
    
         return match;
     };
    

    isValidPassword takes password as an argument and compares it to the user's hashed password using bcrypt's compare method. The method returns a boolean value indicating whether the passwords match.

  3. In src/authentication/passport.js, call isValidPassword to validate the user's password in the login strategy:

     const validate = await user.isValidPassword(password);
    
     if (!validate) {
         return done(null, false, {message: "Wrong Password" });
     }
    

    The complete file should look like this:

     const passport = require("passport");
     const localStrategy = require("passport-local").Strategy;
     const { UserModel } = require("../models");
    
     const JWTstrategy = require("passport-jwt").Strategy;
     const ExtractJWT = require("passport-jwt").ExtractJwt;
    
     passport.use(
         new JWTstrategy(
             {
                 secretOrKey: process.env.JWT_SECRET,
                 jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken()
             },
             async (token, done) => {
                 try {
                     return done(null, token.user);
                 } catch (error) {
                     done(error);
                 }
             }
         )
     );
    
     passport.use(
         "signup",
         new localStrategy(
             {
                 usernameField: "email",
                 passwordField: "password",
                 passReqToCallback: true
             },
             async (req, email, password, done) => {
                 try {
                     const user = await UserModel.create({ 
                         ...req.body, password 
                     });
                     return done(null, user);
                 } catch (error) {
                     done(error);
                 }
             }
         )
     );
    
     passport.use(
         "login",
         new localStrategy(
             {
                 usernameField: "email",
                 passwordField: "password",
                 passReqToCallback: true
             },
             async (req, email, password, done) => {
                 try {
                     const user = await UserModel.findOne({ email });
    
                     if (!user) {
                         return done(null, false, { 
                             message: "User not found" 
                         });
                     }
    
                     const validate = await user.isValidPassword(password);
    
                     if (!validate) {
                         return done(null, false, { 
                             message: "Wrong Password" 
                         });
                     }
    
                     return done(null, user, { 
                         message: "Logged in Successfully" 
                     });
                 } catch (error) {
                     return done(error);
                 }
             }
         )
     );
    

Validation with Joi

To validate user input, we'll be using the Joi library. Joi provides a simple, yet powerful way to define and validate data structures in Node.js applications.

  1. In the /validators directory, create a file called author.validator.js:

     const Joi = require("joi");
    
     const newArticleValidationSchema = Joi.object({
         title: Joi.string().trim().required(),
         body: Joi.string().trim().required(),
         description: Joi.string().trim(),
         tags: Joi.string().trim(),
     });
    
     const updateArticleValidationSchema = Joi.object({
         title: Joi.string().trim(),
         body: Joi.string().trim(),
         description: Joi.string().trim(),
         tags: Joi.string().trim(),
         state: Joi.string().trim(),
     });
    
     const newArticleValidationMW = async (req, res, next) => {
         const article = req.body;
         try {
             await newArticleValidationSchema.validateAsync(article);
             next();
         } catch (error) {
             return next({ 
                 status: 406, 
                 message: error.details[0].message 
             });
         }
     };
    
     const updateArticleValidationMW = async (req, res, next) => {
         const article = req.body;
         try {
             await updateArticleValidationSchema.validateAsync(article);
             next();
         } catch (error) {
             return next({ 
                 status: 406, 
                 message: error.details[0].message 
             });
         }
     };
    
     module.exports = {
         newArticleValidationMW,
         updateArticleValidationMW,
     };
    

    We're exporting two middleware functions, newArticleValidationMW and updateArticleValidationMW. newArticleValidationMW uses the newArticleValidationSchema to verify that the request body of a new article request contains all of the required fields (title, body) and all the provided fields are in the correct format. If all of the fields are valid, it calls the next function to continue with the request. updateArticleValidationMW, is similar to the first, but it uses the updateArticleValidationSchema to validate the request body of an update article request.

    Both functions use the validateAsync method provided by the Joi library to perform the validation. This method takes an object (the request body) and returns a promise that is rejected if the object is invalid or resolved if it is valid.

  2. In the /validators directory, create a file called user.validator.js:

     const Joi = require("joi");
    
     const validateUserMiddleware = async (req, res, next) => {
         const user = req.body;
         try {
             await userValidator.validateAsync(user);
             next();
         } catch (error) {
             return next({ status: 406, message: error.details[0].message });
         }
     };
    
     const userValidator = Joi.object({
         firstname: Joi.string().min(2).max(30).required(),
         lastname: Joi.string().min(2).max(30).required(),
         email: Joi.string().email({
             minDomainSegments: 2,
             tlds: { allow: ["com", "net"] },
         }),
         password: Joi.string()
             .pattern(new RegExp("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})"))
             .required(),
     });
    
     module.exports = validateUserMiddleware;
    
  1. In /validators/index.js:

     const userValidator = require("./user.validator");
     const {
         newArticleValidationMW,
         updateArticleValidationMW,
     } = require("./author.validator");
    
     module.exports = {
         userValidator,
         newArticleValidationMW,
         updateArticleValidationMW,
     };
    

With authentication and validation in place, we're ready to start building the API routes and controllers. In the next article, we'll dive into the details of implementing the routes and controllers for our blog API. Stay tuned!