Hello coders,
I am going to talk about the modern way of token-based authentication using NodeJS, so if you are looking for a start or want to build your own then you have come to right place.
In this post I am going to implement the basic token-based authentication system using ExpressJS, PassportJS and other helpful libraries.
So let's start
First run the command
npm init -y
It will create a package.json file for you with all the necessary defaults
Now we need to install the following packages:
- ExpressJS
- PassportJS
- HelmetJS
- Nodemon
- Passport-JWT
- Passport-Local
- JsonWebToken
- Cookie-Parser
to install these packages run the following command
npm i express helmet jsonwebtoken passport passport-jwt passport-local nodemon cookie-parser
After this we need to make some changes in the package.json file so that we don't have to run the script manually everytime to see the changes. That's exactly where nodemon
comes into the picture. It looks for the changes into the specific file and if there is any change it restarts the server.
Let's add the following script into the scripts section in the package.json
file
"start": "nodemon server.js"
Now let's move ahead. Following is the file & folder structure we are going to follow to organize our code
So far so good. We have organized our code. Now it's time to get our hands dirty by writing some code.
We will start with server.js
file
const express = require("express");
// create app instance from express
const app = express();
// specify the port number for your app
const PORT = 3000;
// home page route
app.get("/", (req, res) => res.send("Hi"));
app.listen(PORT, (req, res) => {
console.log(`Connected on http://localhost:${PORT}`);
});
Now if you run npm run start
in the terminal then your server will start and run on the port 3000.
Note: We are not using database at this point to save ourselves from additional database setup. We are using a simple array of objects user model.
If you need this with database then comment down. I will create another post with the database setup.
So let's create a user with array of objects in users.js
file in the models folder
const users = [
{ username: "Prince", password: "password1" },
{ username: "Kathan", password: "password2" },
{ username: "Srivinay", password: "password3" },
{ username: "Siddharth", password: "password4" },
];
module.exports = users;
Add below html page for login route in the login.html
file in public folder
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NodeJS Authentication</title>
</head>
<body>
<h1>This is the HtML sent by the server</h1>
<a href="/secret">Show Me!</a>
<a href="/auth/google">Google Login</a>
<a href="/auth/logout">Logout</a>
<!-- <img src="./logo.png" alt="logo img" /> -->
</body>
</html>
After this we are going to edit our passport.js
file to add the passportjs with Token based authentication to our app.
const passport = require("passport");
const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
const users = require("./model/users");
// function to extract the cookie from the response header
var cookieExtractor = function (req) {
var token = null;
if (req && req.cookies) {
token = req.cookies["secure_token"];
}
return token;
};
// function to use the jwt as strategy to authenticate the users
function passportAuth() {
try {
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromExtractors([cookieExtractor]),
secretOrKey: "my_secret_key",
},
function (jwt_payload, done) {
const user = users.find((user) => jwt_payload.username === user.username);
if (!user) {
return done(null, false);
}
return done(null, user);
}
)
);
} catch (error) {
throw error;
}
}
module.exports = {
passportAuth,
};
In above code we are first importing the necessary packages then I have create a cookieExtractor
function which is used to extract the cookies from the headers and pass the value of the cookies to passport. Please note that we have used cookies specially to save the authentication tokens so that we don't have to include them seperately in each request if which is the case if we save them in the localStorage or sessionStorage. Sure storing the tokens in cookies have some downside and is susceptible to xsrf but it can be avoided by setting the cookies to httponly, secure and maxAge properties.
After that we using the passportjs
middleware which runs on every request that is beign made to our server and extract the cookies from header. It then decodes the token extracted from cookie using a secretKey which you have to set in .env
file and extracts the user information from tokens. We are verifying here based on the username only for now to keep things simple. If it founds the user then it returns the user's properties or else returns false.
Now let's create the login routes
Add following code to loginRoutes.js
file in the routes folder
const express = require("express");
const loginRouter = express.Router();
//importing the getLoginForm and postLoginDetails from controllers/loginController.js file
const { getLoginForm, postLoginDetails } = require("../controllers/loginController");
// router for the GET method of the login form
loginRouter.get("/", getLoginForm);
// route for the POST method of the login form
loginRouter.post("/", postLoginDetails);
module.exports = loginRouter;
After this we are going to edit the loginController.js
file in the controllers folder
const path = require("path");
const jwt = require("jsonwebtoken");
const users = require("../model/users");
// function to serve the login page to the user
const getLoginForm = (req, res) => {
res.sendFile(path.join(__dirname, "..", "public", "login.html"));
};
// function to handle the login request
const postLoginDetails = (req, res) => {
// extracting the username and password from the body of the request
const { username, password } = req.body;
const user = users.find((user) => user.username === username);
// sending 404 error along with the message if user is not found in the database
if (!user) {
return res.status(404).json({ message: "INCORRECT_USERNAME" });
} else {
// generating the token and with the information and sending it to the user on the client end
const token = jwt.sign({ username: username }, "my_secret_key");
// setting the token in the cookie so that we don't need to include it manually in each request
res.cookie("secure_token", token, {
httpOnly: true, // Ensures cookie cannot be accessed by JavaScript
maxAge: 1000 * 60 * 60, // Expire in 1 hour,
});
// returning the response containing the token and the message
return res.json({
message: "sucessfully authentiacted",
token,
});
}
};
// function to add the authentication on the selective routes using the jwt strategy
function authenticate(req, res, next) {
// Use Passport to authenticate the request using the "jwt" strategy
passport.authenticate("jwt", { session: false }, (err, user) => {
console.log(user);
if (err) next(err); // If there's an error, pass it on to the next middleware
if (!user) {
// If the user is not authenticated, send a 401 Unauthorized response
return res.status(401).json({
message: "Unauthorized access. No token provided.",
});
}
// If the user is authenticated, attach the user object to the request and move on to the next middleware
req.user = user;
next();
})(req, res, next);
}
module.exports = {
passportAuth,
authenticate,
};
In above code we are importing the necessary packages. After that we are serving the login.html file which is served to the user after he visits the /login
route in the getLoginForm
function.
Now moving on we have postLoginDetails
function which first extracts the user's username and password from the request, then looks for the that user in the users model. If it founds the user then it creates a token using the users information and then sets that token in the cookie. We have already talked about why we are using cookies approach. And finally send the successful message to the client side. In case if user is not found then also we are sending "INCORRECT_USERNAME"
message to the client side.
After that we have added the authenticate
function middleware which you can use to implement the authentication on selective routes. It first extracts the JWT tokens with the help of passportjs and looks for user. If user is found then it returns the user or returns the error message.
Now going back to server.js
file and adding the passportjs middleware and login routes
Modifying our existing file like this
const express = require("express");
const helmet = require("helmet");
const passport = require("passport");
const cookieParser = require("cookie-parser");
const { passportAuth } = require("./passport");
const loginRouter = require("./routes/loginRouter");
// create app instance from express
const app = express();
// specify the port number for your app
const PORT = 3000;
// using this helmet middleware to set extra attributes on the request headers
app.use(helmet());
// middleware to parse the cookies in the incoming requests
app.use(cookieParser());
// including the passportjs middleware
passportAuth();
app.use(express.urlencoded({ extended: false }));
// initialising the passportjs middleware
app.use(passport.initialize());
// home page route
app.get("/", (req, res) => res.send("Hi"));
// login route
app.use("/login", loginRouter);
// secret page route which requires the user to be authenticated
app.get("/secret", authenticate, (req, res) => {
res.send("secret value is 19");
});
Here we have made couple of changes. We have added the helmet middleware so set extra necessary headers, cookieParser middleware to parse the cookies from the requests and added the passportjs middleware. After that we are intializing the passportjs middleware. Lastly we are creating the routes for the /login route.
At this point we have made all the heavy lifting.
Now if you go the http://localhost:3000/
route you should see "Hi".
If you go to http://localhost:3000/secret
endpoint then you will get the error message saying "Unauthorized access. No token provided". It means our authentication is working.
Now go to http://localhost:3000/login
and you will see the two input fields in which first field is for username and last field is for password.
When you enter any username listed in the users model you will get token and successful message. If you enter wrong username then you will get error.
After this if you again go to http://localhost:3000/secret
endpoint you will get you secret endpoint's content which in our case is 'Your secret value is 19'.
Woo hoo! You have successfully added the modern token based authentication in your app.
I hope this article gave you the basic knowledge about implementing authentication logic in your app.
There are still many areas of improvement in the code like verifying the password along with username, adding database support , signup process and many more.
So try them on your own and if stuck, comment below, I will create another article along with github repo for you.
Happy Coding!! :)
Top comments (0)