21
How to Secure your API's Routes with JWT Token
Imagine yourself registering for a few days long conference about JavaScript. Before you go there, you have to enter your information and get a ticket. Once you reach the conference, security checks your ticket, ID, and give you a special "guest card". With that card, you can enter the conference area, leave it, and come back whenever you want. You don't have to give all of your personal information over and over again, nor show your ticket and ID. How is that? It all thanks to the "guest card". Now think, what if there were no tickets nor "ID cards" for such events. Then you would have to prove your credentials in a very tedious way, every time you enter the area.
In terms of web applications, this situation is not very different. Some of the paths on various websites are visible only for the registered users. It would be very impractical to ask the user to log in on each different route. One of the solutions can be storing cookies and sending them back and forth between the client and the server. Another way is called authorization token. To be more precise, JWT - JSON Web Token.
These days, JWT tokens became one of the most popular and practical ways of authenticating users. So what are those JWT tokens? It is nothing else than a long string with the encoded data, that can be decoded on the server-side. Each JWT token consists of 3 main parts:
- Header: type of algorithm
- Payload: additional data
- Signature: verification
JWT tokes have two main purposes, and those are Authorization and Information Exchange. For example, when the user logs in on our website, JWT tokes is generated by the server, added to the given user in the database, and sent back to the client. On the client-side, we can store the JWT token in the localstorage for example and add it to headers in form of Authorization: Bearer <JWT Token>
In this case, we can easily authenticate the user, and also decided whether we should give access to the given route or not.
In the previous tutorials, we were building a very simple REST API server for storing users in the Mongo database. That is why, in this tutorial, we will use the same code and extend it with an additional feature. However, if you have your code it is also OK to just implement the given parts inside of your code. Let's open the code editors and start coding.
First of all, we will have to install JWT dependency, with the following command:
npm i jsonwebtoken
Later, inside of the user schema, we will need another field for the token itself.
accessToken: { type: String, default: null }
After adding the dependency and accessToken
field to the model, we are ready to move on. In the "middlewares" folder, create a new file called "generateJWT.js".
The code should look like that:
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
dotenv.config();
const options = {
expiresIn: "24h",
};
async function generateJWT(username) {
try {
const payload = { username };
const token = await jwt.sign(payload, process.env.JWT_SECRET, options);
return { error: false, token };
} catch (error) {
return { error: true };
}
}
export default generateJWT;
Our function will take one parameter and it will be the username, that will be added to the payload. You may have also realised that we need a SECRET to sign the JWT token. Since it's very sensitive data, it's better to keep it inside of the ".env" file. Inside of the .env file, add a variable called JWT_SECRET="<your secret string>"
and add a secret string of your own preference.
Great, so now our JWT token generating function is ready and everything is set up. Let's add the functionality inside of the "login" method.
const { error, token } = await generateJWT(user.username);
if (error) {
return res.status(500).json({
error: true,
message: "Couldn't create access token. Please try again later.",
});
}
user.accessToken = token;
await user.save();
Add the code above, right after comparing the passwords. On each of the login, the server will generate a new JWT token, add it to the user object and save it in the database.
So far we can log in and create a new JWT token, but where can we use it now? For example, we can protect given routes with a JWT token or execute some actions based on the JWT token. But before we do that, we have to check if the JWT token is real and valid. To make it happen, we will add validateToken middleware, in between the route and controller.
Inside of the "middlewares" folder, create a new file called "validateToken.js" and add the following code inside.
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
dotenv.config();
import User from "../models/user.model.js";
async function validateToken(req, res, next) {
const auhorizationHeader = req.headers.authorization;
let result;
if (!auhorizationHeader) {
return res.status(401).json({
error: true,
message: "Access token is missing",
});
}
const token = req.headers.authorization.split(" ")[1];
const options = {
expiresIn: "24h",
};
try {
let user = await User.findOne({
accessToken: token,
});
if (!user) {
result = {
error: true,
message: "Authorization error",
};
return res.status(403).json(result);
}
result = jwt.verify(token, process.env.JWT_SECRET, options);
if (!user.username === result.username) {
result = {
error: true,
message: "Invalid token",
};
return res.status(401).json(result);
}
req.decoded = result;
next();
} catch (error) {
console.error(error);
if (error.name === "TokenExpiredError") {
return res.status(403).json({
error: true,
message: "Token expired",
});
}
return res.status(403).json({
error: true,
message: "Authentication error",
});
}
}
export default validateToken;
In short, firstly we check whether the JWT token is present in the headers, then we split the string and take the token itself (therefore split method). After that, we check is there any user with a given token inside of the database, and did it expire or not. If everything is OK, then decoded token is added to the request part, and our middleware calls "next()" to move on to the next middleware or controller method.
Where can we use it now? First of all, we can add a third method to our controller called "logout", so that we can erase the JWT token on the logout.
Go to the UserController.js file and add method "logout":
async logout(req, res) {
try {
const { username } = req.decoded;
let user = await User.findOne({ username });
user.accessToken = "";
await user.save();
return res.status(200).json({
success: true,
message: "User logged out",
});
} catch (error) {
console.error(error);
return res.status(500).json({
error: true,
message: error,
});
}
}
Now we can move back to the routes and add the last missing part, which is naturally the logout route. The whole usersRouter.js
file should look like that:
import express from "express";
import UsersController from "../controllers/UsersController.js";
const usersRouter = express.Router();
import cleanBody from "../middlewares/cleanBody.js";
import validateToken from "../middlewares/validateToken.js";
const users = new UsersController();
usersRouter.post("/signup", cleanBody, users.signup);
usersRouter.patch("/login", cleanBody, users.login);
usersRouter.patch("/logout", validateToken, users.logout);
export default usersRouter;
That's all! You can turn on the server, open Postman and check the new routes.
JWT tokens are a crucial part of any serious application, with the ready code you can extend it to many other routes and methods. Feel free to modify the code, and add your own parts. There are still a few missing parts, like unit testing, security issues, CORS or connecting the backend with the frontend. Stay tuned for more and let me know if you have some questions or suggestions.