DEV Community

Sagar Medtiya
Sagar Medtiya

Posted on • Originally published at blog.sagarmedtiya.me on

Meet my latest project, I built User Authentication using Passport.js and Bcrypt๐ŸŽ‰

image
Prerequisites : Javascript ES6

๐ŸPassport.js

It is an authentication middleware for Express-based app using Node.js. Being a middleware, it has access to request and response object and can manipulate them into request-response cycle. The passport.js library consist of two separate libraries. The first is the main Passport JS library, and the second is the relevant strategy library.

  • The primary Passport JS library is always required, and is used to maintain session information for authenticated users (i.e. you will import this library irrespective of the type of Strategy that you will use to authenticate the user).
  • The secondary strategy library is dependent on the methodology you plan use to authenticate a user. eg. passport-local, passport-facebook, passport-oauth-google etc. Users could be authenticated against a username/password saved in a database that you created either locally or on a cloud (called Local Strategy), OR users could be authenticated by having them log into their Google account (Google Authentication Strategy), or Facebook, or Github account.

The Passport JS framework abstracts the Login process into 2 separate parts, the session management (done by the Passport JS library ), and the authentication (done by the secondary Strategy library eg. passport-local or passport-facebook or passport-oauth-google etc.)

Here comes the express.js,

The Passport JS library connects with the express-session library, and forms the basic scaffolding to attach the (authenticated) user information to the req.session object. The main Passport JS library deals with already authenticated users, and does not play any part in actually authenticating the users. Its sole purpose is to maintain (attach) the (already authenticated) user to sessions.

๐ŸBcrypt

It is very much important to have a secure system to save the password on the internet business. Saving password directly on the database is not recommended. There can be a security breach. In order to save the password without any security breach, and providing the users with the correct privacy of their personal data, we need to follow some protocols when building an app.

There comes hashing , developers usually run a function to convert this password into something that looks completely different from its original form through a mathematical algorithm. This process is called hashing and the mathematical algorithm is called hashing algorithm. The famous package of node.js is "bcrypt" and it is downloaded on an average of 590,489 times a week (oh yeah, thats a lot ๐Ÿ’ฐ).

Bcrypt also introduces salt technique. In order to increase the complexity of password security and protect users passwords from the attacks, an additional step called password salting is taken. Salting a hash, in the field of cryptography, actually means to add an additional string of 32 or more characters to the password before it gets hashed.

Lets demonstrate it with an example.

Michael and Bob both use the same password s@1t3dH@shBrown by coincidence, they will also have the same hash: $2a$12$xdWgQ5mhv8rSaUK3qdusTO4XdMFbQi6TD/1VvOZjvGm10RXnhZZa2

However, if Michaels password is salted with Iwx2ZEand Bobs password is salted with 0DoVej, they will have completely different salted hashes.

Michael

Password: s@1t3dH@shBrown

Salt: Iwx2ZE

Salted Input: Iwx2ZEs@1t3dH@shBrown

Salted Hash Output: $2a$12$TGRg8FCZvnDm.f4WPNtWQucwRv5zsi4D9Qy/gYgpfFfYx9XpXdE6a

Bob

Password: s@1t3dH@shBrown

Salt: 0DoVej

Salted Input: 0DoVejs@1t3dH@shBrown

Salted Hash Output: $2a$12$VtpXTHf69x1db/71bGHl3eMiEDAkgQe/Gq6UeNOKuHvdg.WnIXEHa

As you can see, their salted hash outputs are quite different even though they share the same password.

๐ŸLet's build User Authentication

๐Ÿ€Install the required package and run the server

Type npm init and install the below packages,

  • express
  • express-session
  • passport
  • passport-local
  • bcryptjs

Now create a file, app.js, and add the following code to create the server.

const express = require("express");
const app = express();
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});

Enter fullscreen mode Exit fullscreen mode

You can use dotenv to store the PORT number.

๐Ÿ€Set up the Database

We'll use mongoDB, so let's install it

npm i mongoose

create db.js file and configure the database

const mongoose = require('mongoose')

function Dbconnect(){

    const url = mongodb://localhost:27017/user;
    mongoose.connect(url,{ useNewUrlParser: true, useUnifiedTopology: true
        }).then(()=>{
            console.log('Connection Successful');
        }).catch((error)=>{     
            console.log('Something went wrong', error)
        });
}

module.exports = Dbconnect

Enter fullscreen mode Exit fullscreen mode

Now, create UserModel.js and add the following.

const mongoose = require('mongoose')
const {Schema} = mongoose
const UserSchema = new Schema ({
 email: {
 type: String,
 required: true
 },
 password: {
 type: String,
 required: true
 }
})
const UserModel = mongoose.model('user', UserSchema);
module.exports = UserModel;

Enter fullscreen mode Exit fullscreen mode

Before storing the password, you need to encrypt it for security purposes. You will use bcrypt , a very useful npm package that makes working with encrypted passwords easy.

npm i bcrypt

Modify UserModel.js to encrypt the password before saving it to the database.

const mongoose = require('mongoose')
const bcrypt = require('bcryptjs');
const {Schema} = mongoose

const UserSchema = new Schema ({
...
})
UserSchema.pre('save', async function(next) {
 try {
 // check method of registration
 const user = this;
 if (!user.isModified('password')) next();
 // generate salt
 const salt = await bcrypt.genSalt(10);
 // hash the password
 const hashedPassword = await bcrypt.hash(this.password, salt);
 // replace plain text password with hashed password
 this.password = hashedPassword;
 next();
 } catch (error) {
 return next(error);
 }
 });
...
const User = mongoose.model('User', UserSchema);

Enter fullscreen mode Exit fullscreen mode

Here we are using a pre save hook to modify the password before it is saved. The idea is to store the hash version of the password instead of the plain text password. A hash is a long complex string generated from a plain text string.

Use isModified to check whether the password is changing since you only need to hash new passwords. Next, generate a salt and pass it with the plain text password to the hash method to generate the hashed password. Finally, replace the plain text password with the hashed password in the database.

In app.js , connect to the database.

// connect to db
const db = require('./db');
db.connect();

Enter fullscreen mode Exit fullscreen mode

๐Ÿ€Set up Passport

Install Passport and passport-local. You will use these packages to register and login users.

npm i passport
npm i passport-local

Enter fullscreen mode Exit fullscreen mode

Create a new file, passportConfig.js, and import passport-local and the UserModel.js.

const LocalStraregy = require("passport-local").Strategy;
const User = require("./userModel");

Enter fullscreen mode Exit fullscreen mode

Configure Passport to handle user registration.

const LocalStrategy = require("passport-local");
const User = require("./userModel");
module.exports = (passport) => {
 passport.use(
 "local-signup",
 new LocalStrategy(
 {
 usernameField: "email",
 passwordField: "password",
 },
 async (email, password, done) => {
 try {
 // check if user exists
 const userExists = await User.findOne({ "email": email });
 if (userExists) {
 return done(null, false)
 }
 // Create a new user with the user data provided
 const user = await User.create({ email, password });
 return done(null, user);
 } catch (error) {
 done(error);
 }
 }
 )
 );
}

Enter fullscreen mode Exit fullscreen mode

In the above code, you are checking if the email is already in use. If the email does not exist, register the user. Note that you are also setting the username field to accept an email. By default, passport-local expects a username, so you need to tell it you are passing in an email instead.

Use passport-local to also handle user login.

module.exports = (passport) => {
 passport.use(
 "local-signup",
 new localStrategy(
 ...
 )
 );
passport.use(
 "local-login",
 new LocalStrategy(
 {
 usernameField: "email",
 passwordField: "password",
 },
 async (email, password, done) => {
 try {
 const user = await User.findOne({ email: email });
 if (!user) return done(null, false);
 const isMatch = await user.matchPassword(password);
 if (!isMatch)
 return done(null, false);
 // if passwords match return user
 return done(null, user);
 } catch (error) {
 console.log(error)
 return done(error, false);
 }
 }
 )
 );
};

Enter fullscreen mode Exit fullscreen mode

Here, check whether the user exists in the database, and if they do, check if the password provided matches the one in the database. Note you also call the matchPassword() method on the user model so go to UserModel.js file and add it.

UserSchema.methods.matchPassword = async function (password) {
 try {
 return await bcrypt.compare(password, this.password);
 } catch (error) {
 throw new Error(error);
 }
};

Enter fullscreen mode Exit fullscreen mode

This method compares the password from the user and the one in the database and returns true if they match.

๐Ÿ€Set Up Authentication Routes

You now need to create the endpoints to which users will send data. First up is the signup route which will accept the email and password of a new user.

In app.js , use the passport authentication middleware you just created to register the user.

app.post("/auth/signup",
 passport.authenticate('local-signup', { session: false }),
 (req, res, next) => {
 // sign up
 res.json({
 user: req.user,
 });
 }
);

Enter fullscreen mode Exit fullscreen mode

If successful, the signup route should return the created user.

Next, create the login route.

app.post(
 "/auth/login",
 passport.authenticate('local-login', { session: false }),
 (req, res, next) => {
 // login
 res.json({
 user: req.user,
 });
 }
);

Enter fullscreen mode Exit fullscreen mode

๐Ÿ€Add Protected Routes

So far, you have used Passport to create a middleware that registers a user in the database and another that allows a registered user to sign in. Next, you will create an authorization middleware to protect sensitive routes using a JSON web token(JWT). To implement JWT authorization, you need to:

Generate JWT token. Pass the token to the user. The user will send it back in authorization requests. Verify the token sent back by the user.

You will use the jsonwebtoken package to handle JWTs.

npm i jsonwebtoken

Next, generate a token for each user that successfully logs in.

In app.js , import jsonwebtoken and modify the login route like below.

app.post(
 "/auth/login",
 passport.authenticate('local-login', { session: false }),
 (req, res, next) => {
 // login
 jwt.sign({user: req.user}, 'secretKey', {expiresIn: '1h'}, (err, token) => {
 if(err) {
 return res.json({
 message: "Failed to login",
 token: null,
 });
 }
 res.json({
 token
 });
 })
 }
);

Enter fullscreen mode Exit fullscreen mode

In a real-life application, you would use a more complicated secret key and store it in a configuration file.

The login route returns a token if successful.

Use passport-jwt to access protected routes.

npm i passport-jwt In passportConfig.js , configure the passport-jwt.

const JwtStrategy = require("passport-jwt").Strategy;
const { ExtractJwt } = require("passport-jwt")
module.exports = (passport) => {
passport.use(
 "local-login",
 new LocalStrategy(
 ...
 );
passport.use(
 new JwtStrategy(
 {
 jwtFromRequest: ExtractJwt.fromHeader("authorization"),
 secretOrKey: "secretKey",
 },
 async (jwtPayload, done) => {
 try {
 // Extract user
 const user = jwtPayload.user;
 done(null, user);
 } catch (error) {
 done(error, false);
 }
 }
 )
 );
};

Enter fullscreen mode Exit fullscreen mode

Notice you are extracting the JWT from the authorization header instead of the request body. This prevents hackers from intercepting a request and grabbing the token.

Now You're Ready to Take Your User Authentication to the Next Level

I hope you have like this small tutorial. Happy coding๐Ÿ’—

Always remember, no one reached to the top in one shot. It took them a lot more struggle and hard work than you can imagine. So strive for knowledge, and keep moving forward. Thank you

Top comments (0)