User Authentication in NodeJS Using Passport and MongoDB
If you want to protect sensitive content in your Node application, you need a way to authenticate users. However, building your own authentication system is complex, and time-consuming and if not done correctly can introduce security vulnerabilities in your application. Third-party tools like Passport make authentication easier.
In this tutorial, you will learn how to implement authentication in Node using Passport and MongoDB.
Nội Dung Chính
What Are Authentication and Authorization?
While authentication and authorization are sometimes used interchangeably, these two security concepts have different meanings. Authentication is the process of verifying a user is who they claim to be while authorization is the process of determining whether an authenticated user has access to certain parts of your application.
What Is Passport.js?
Passport.js (or Passport) is an authentication middleware for NodeJS that provides more than 500 strategies for authenticating users including passport-local which uses a username and password.
This tutorial uses passport-local and passport-jwt to secure routes.
How to Set Up User Authentication in NodeJS
Now you know a little about user authentication and Passport.js, we can look at how to set up authentication on NodeJS. Below, we’ve outlined the steps you’ll need to take.
Step 1: Set Up a Node Server
Create a folder named user-auth-nodejs and navigate to it using your terminal.
mkdir user-auth-nodejscd user-auth-nodejs
Next initialize package.json.
npm init
Since you will be using Express, a NodeJS backend framework, install it by running the following command.
npm i express
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}
`);
});
Related: Learn How to Install Npm and Node.js on Ubuntu
Step 2: Set Up the Database
You need a database to store user data. You will use mongoose to create a MongoDB data schema that defines the structure and type of data you will store in the database. Since you are storing user data, create a user schema.
Install mongoose.
npm i mongoose
Create a new file, 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;
Related: How to Create a Database and Collection in MongoDB
Before storing the password, you need to encrypt it for security purposes. You will use bcryptjs, a very useful npm package that makes working with encrypted passwords easy.
Install bcryptjs.
npm i bcryptjs
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
{
const
user = this
;
if
(!user.isModified('password'
)) next();
const
salt = await
bcrypt.genSalt(10
);
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);
Here you 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.
Create db.js and configure the database.
const
mongoose = require
("mongoose"
);
mongoose.Promise = global
.Promise;
const
dbUrl = "mongodb://localhost/user"
;
const
connect = async
() => {
mongoose
.connect
(dbUrl
, { useNewUrlParser
: true, useUnifiedTopology: true });
const
db = mongoose.connection;
db.on("error"
, () => {
console
.log("could not connect"
);
});
db.once("open"
, () => {
console
.log("> Successfully connected to database"
);
});
};
module
.exports
= { connect };
In app.js, connect to the database.
const
db = require
('./db'
);
db
.connect
();
Step 3: 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
Create a new file, passportConfig.js, and import passport-local and the userModel.js.
const
LocalStraregy = require
("passport-local"
).Strategy;
const
User = require
("./userModel"
);
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);
}
}
)
);
}
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
);
return
done(null
, user);
} catch
(error) {
console
.log(error)
return
done
(error, false
);
}
}
)
);
};
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);
}
};
This method compares the password from the user and the one in the database and returns true if they match.
Step 4: 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) => {
res
.json
({
user
: req
.user
,
});
}
);
Related: Authentication vs. Authorization: What is the Difference?
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) => {
res
.json
({
user
: req
.user
,
});
}
);
Step 5: 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.
Run the following command to install it.
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) => {
jwt.sign({user: req.user}, 'secretKey'
, {expiresIn: '1h'
}, (err, token) => {
if(err) {
return
res.json({
message: "Failed to login"
,
token: null
,
});
}
res
.json
({
token
});
})
}
);
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
{
const
user = jwtPayload.user;
done(null
, user);
} catch
(error) {
done
(error, false
);
}
}
)
);
};
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.
To see how passport-jwt guards routes, create a protected route in app.js.
app
.get
(
"/user/protected"
,
passport
.authenticate
("jwt
", { session
: false }),
(req, res, next) => {
res
.json
({user
: req.user});
}
);
Only a request with a valid JWT returns the user data.
Now You’re Ready to Take Your User Authentication to the Next Level
In this tutorial, you learned how you can authenticate users using an email and a password with the help of Passport. It might seem daunting at first, but the process is relatively straightforward. You can go even further and use third-party identity providers supported by Passport such as Twitter, Facebook, and Google.