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. 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.
- For the
/signup
route, we'll set up a local strategy, which uses thepassport-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 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.
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
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.
- 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!
Top comments (0)