Building a Blog API with Node.js: Authentication and Validation (Part 3)
Table of contents
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.
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. TheExtractJWT.fromAuthHeaderAsBearerToken
method will extract the JWT from theAuthorization
header of the request as a Bearer token. If the JWT is found, it will be decoded and passed to the function along with thedone
function. The function will check theuser
object in the JWT to see if it is valid. If it's valid, it will call thedone
function with theuser
object, indicating that the user has been authenticated, else it will call thedone
function with an error.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 newUser
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); } } ) );
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 theuser
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); } } ) );
We still need to verify the user's password. In
/src/models/user.models.js
, create a method calledisValidPassword()
in theUser
model:UserModel.methods.isValidPassword = async function (password) { const user = this; const match = await bcrypt.compare(password, user.password); return match; };
isValidPassword
takespassword
as an argument and compares it to the user's hashed password usingbcrypt
'scompare
method. The method returns a boolean value indicating whether the passwords match.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.
In the
/validators
directory, create a file calledauthor.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
andupdateArticleValidationMW
.newArticleValidationMW
uses thenewArticleValidationSchema
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 theupdateArticleValidationSchema
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.In the
/validators
directory, create a file calleduser.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;
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!