Introduction
Authentication is a crucial part of web development. It is one of the ways developers ensure the security of an application. Though authentication and authorization are often used interchangeably, they are not the same thing. Authentication is simply the process of determining who a user is while authorization on the other hand is the process of determining if a user has access to a particular resource he/she is requesting.
JSON Web Tokens have become a popular choice for implementing authentication in web applications due to their simplicity, ease of implementation, and effectiveness.
Prerequisites
Before we begin, ensure you have Node.js and npm (Node Package Manager), MongoDB, and Postman installed locally. A little knowledge of the tools listed above is also required.
Project setup
Start by creating a directory for the project. Open your terminal and type the command
mkdir passport-jwt-express-auth
.Navigate to the directory using the command
cd passport-jwt-express-auth
.Initialize a node project using the command
npm init -y
Install the dependencies by running
npm install express jsonwebtoken nodemon passport passport-jwt bcrypt dotenv mongoose
.bcrypt
would be used to hash the user passwords,jsonwebtoken
would be used for signing tokens,passport-jwt
would be used for retrieving and verifying tokens,nodemon
would be used automatically restart the server during development,dotenv
would be used to access environment variables,mongoose
would be used as the MongoDB ODM andexpress
would be used create the server.Create a
.env
file at the root of the project to hold the project environment variables. The main environment variables we would use in the project would beJWT_SECRET
,PORT
, andMONGO_URI
.JWT_SECRET
would hold the secret which our application would use to sign tokens,PORT
would hold the port number in which we would serve our application,MONGO_URI
would hold the link to the Mongodb database.Open the
package.json
file and add the following."type": "module"
This would enable you to use es6 imports in the project. Next, add the following under the scripts section
"start:dev": "nodemon src/app.js",
"start:prod": "node src/app.js"
These scripts would be used to start the express server.
Create a folder to hold the source code using
mkdir src
.Navigate to the folder using the command
cd src
and create four files and name themapp.js
,passport.js
,model.js
, andauth.js
.The
auth.js
file would hold the authentication routes, thepassport.js
file would hold the configurations for passport, themodel.js
file would hold the user model and theapp.js
file would hold the code to bootstrap the server.
Setting up the express server
- Open the
app.js
file and type the code below.
import express from "express";
import authRouter from "./auth.js";
import dotenv from 'dotenv';
// configure dotenv to access environment variables
dotenv.config();
const PORT = process.env.PORT || 3000;
// setup express server
const server = express();
server.use(express.json());
server.use("/api/v1/auth", authRouter);
// listen for connections
server.listen(PORT, () => {
console.log(`server is listening on port ${PORT}`);
});
The code above sets up the express server and attaches the authentication routes (we have yet to create them) to the server.
Setting up the database and schema
- Open the
model.js
file and create the user schema. The schema establishes the fields and types of data to be stored in the Mongodb database. Create the user schema by typing the code below.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
});
const UserModel = mongoose.model('user', UserSchema);
module.exports = UserModel;
The schema above specifies that the database would have an email and password field which would both have String types and be required. The
unique: true
property in the email field ensures that the database does not store two similar emails. The Mongoose library takes the schema and converts it to a model that would be used to perform CRUD actions later on.Open the
app.js
file and update it with the code below to enable the server to connect to the Mongodb database when the server starts up.
import mongoose from "mongoose";
// MongoDB connection uri
const MONGO_URI =
process.env.MONGO_URI ||
"mongodb://127.0.0.1:27017/passport-jwt-express-auth";
// connect to MongoDB
mongoose
.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("successfully connected to mongodb");
})
.catch((err) => {
console.log(err);
});
We first imported the Mongoose library and created the MONGO_URI variable to hold the connection string of our MongoDB database which was gotten by accessing the
MONGO_URI
environment variable.We then used the connection string to connect to the database.
Configuring passport and JWT
- Open the passport.js file and type the following code
import { ExtractJwt, Strategy } from "passport-jwt";
import passport from "passport";
const UserModel = require("./model.js");
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: "secret",
};
passport.use(
new Strategy(opts, async (payload, done) => {
try {
const user = UserModel.findById(payload.id);
if (user) return done(null, user);
} catch (error) {
return done(error);
}
})
);
The code above uses the passport and the passport-jwt
strategy to extract the JSON Web Token from the request header and verifies using the JWT secret which can be gotten from the environment variables. If the token is valid, the ID of the user which is gotten from the token is then used to find and return the user's details from the database.
Implementing authentication routes
- We would implement the register route first. It is important to note that we would not be using best practices as it is out of the scope of this post.
import express from "express";
import UserModel from "./model.js";
const authRouter = express.Router();
authRouter.post("/register", async (req, res, next) => {
try {
const user = await UserModel.create({
email: req.body.email,
password: req.body.password,
});
return res.status(201).json({
message: "user created",
user: { email: user.email, id: user._id },
});
} catch (error) {
console.log(error);
}
});
In the register route, we simply create the user and return a response that contains a message, the user's email, and id. It is important to note that in a real-world app, the password would be hashed as well as validations performed. In the next step, we'll create the login route.
authRouter.post("/login", async (req, res, next) => {
try {
//check if user exists
const userExists = await UserModel.findOne({ email: req.body.email });
if (!userExists)
return res.status(400).json({ message: "user does not exist" });
// check if password is correct
if (userExists.password !== req.body.password)
return res.status(400).json({ message: "incorrect password" });
// generate access token
const accessToken = jwt
.sign(
{
id: userExists._id,
},
"secret",
{ expiresIn: "1d" }
)
return res
.status(200)
.json({ message: "user logged in", accessToken: accessToken });
} catch (error) {
console.log(error);
next(error);
}
});
In the login route, we first check to see if the user exists and then check if the user's password is correct. After that, we sign an access token using the user's id, a secret key which would be stored in the .env file as well as specify a time limit for the access token to be valid. In the next code snippet, we would create a route to return the user's profile. This is the route that would be protected using JWT.
authRouter.get("/profile", async (req, res, next) => {
try {
// check if user exists
const userExists = await UserModel.findOne({ email: req.body.email });
if (!userExists)
return res.status(400).json({ message: "user does not exist" });
return res
.status(200)
.json({ userId: userExists._id, email: userExists.email });
} catch (error) {
console.log(error);
next(error);
}
});
In this route, all we simply do is to check if the user exists using the user's email and then return the user's id and email.
Protecting routes
- To protect the route to get a user's profile, we first import passport and the passport strategy implemented earlier on into the routes file.
import passport from "passport";
import "./passport.js";
- In the next step, we use the passport package to authenticate the route to get a profile and ensure only authenticated users can access a particular route. We update the route by adding the line
passport.authenticate("jwt", { session: false }),
. Your code should be like the snippet below:
authRouter.get(
"/profile",
passport.authenticate("jwt", { session: false }),
async (req, res, next) => {
// leave as before
})
Testing
The next step would involve testing the routes in the project.
- Start the server by running the code
npm run start:dev
. - Use Postman to create a user by navigating to the route
localhost:3000/api/v1/auth/register
. Change the port number if yours is different. - Login by navigating to the route
localhost:3000/api/v1/auth/register
and passing your email and password - Try accessing the profile route without passing the
bearer
param which would hold the token of the user and an error is returned. However, if you include thebearer
param with the correct access token which is provided when you login, the route returns the user's id and email.
Conclusion
In this tutorial, we learned the importance of authentication and how to implement a basic authentication system using JSON Web Tokens (JWT) and Passport in a Node.js application.
We started by setting up the project, installing necessary dependencies, and configuring our Express server to handle authentication routes. We also established a MongoDB database schema for user data and integrated Passport for JWT authentication.
It is important to note that while this tutorial provides a solid foundation, real-world authentication systems require additional features such as password hashing, input validation, error handling, and more comprehensive testing. Building upon this foundation, you can explore advanced authentication practices and further enhance the security of your web applications.
Feel free to refer to the provided code and adapt it to your specific project needs.
Top comments (1)
good