DEV Community

Cover image for Securing Node.js Applications with JSON Web Tokens(JWT)
Taslan Graham
Taslan Graham

Posted on • Edited on • Originally published at taslangraham.com

Securing Node.js Applications with JSON Web Tokens(JWT)

Full article can be found here.

Security is a critical component of most modern web applications. As you progress through your career, you will develop more and more applications which requires some level of security, namely authentication and authorization. One way to handle security in your application is through the use of JSON Web Tokens (JWT) which we'll discuss in this article.

First, there are some fundamental things that we need to cover before we start writing code.

What is Authentication?

Image by Achin Verma from Pixabay

In simple terms, authentication is verifying that a person or an entity is who it claims to be. A common way of authentication is combination of email/username with a password to log into web applications. After entering the combination of email/username with a password, the application checks to verify that this combination is correct, essentially authenticating the user. Access is granted only if the combination is correct.

What is Authorization?

Authorization determines the privileges or access levels that an authenticated user has on resources. Resources includes computer programs, files, services, data and application features.

JSON Web Token

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. -Jwt.io

What does this mean in simple terms? A JWT is an encoded string (token) which can be share between a server and client. The encoded string can hold data inside what is called a payload. This information, though protected against tampering, is readable by anyone. Do not put secret information in the payload or header elements of a JWT unless it is encrypted.

How JWT works in securing our application?

Project setup

Image by Arek Socha from Pixabay

With all the background knowledge out of the way, it is time for the fun stuff! We are getting closer to writing some code, but first we have to set-up our project.

First, create a folder named node-jwt-example. Open your text editor of choice, I'm using VS Code, and open node-jwt-example folder.

We'll need to install a couple packages. Inside your project's root directory, open your terminal and run the following commands :

  1. npm init - this will initialize your project and create a package.json file. Press enter and accept all the default settings.
  2. npm install express --save - express will handle all our routing
  3. npm install jsonwebtoken --save - install the JSON Web Token package for node
  4. npm install bcryptjs --save - will be used to hash our passwords
  5. npm install body-parser - parses incoming requests
  6. npm install mongoose - mongoose is used to interact with our MongoDb database
  7. npm install nodemon - automatically restarts server each time we save our changes

Now create the following folder structure

Lets Code! 👨🏾‍💻 

Inside your app.js file, copy and paste the following code.

const express = require('express');
const app = express();

const bodyParser = require("body-parser"); //use to parse incoming request bodies 
const db = require("./db");

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

const userRoutes = require('./routes/users');
app.use('/user', userRoutes); //tells express to forward user routes to routes/users.js

module.exports = app; // this should be the last line in your file

Here we are importing our express library. We've setup body-parser. We've also included our users.js routes file which will handle all /user routes. We've also required in our db.js file which will hold the configuration for our database. Finally we've exported our app.js to make it accessible in other files.

Next, let us set up our server. Paste the following code inside your server.js file.

const http = require('http');
const app = require('./app');

const port = 5000; //selects process port or defaults it to 5000
const server = http.createServer(app);

server.listen(port, () => {
    console.log("listening on port " + port);
});

Here we are setting up our server and assigning it a port number (5000). The serverlisten() method creates a listener on the specified port. We then log a message to the console to signal that our server has been successfully set-up;

Next, we will add the basic set-up for our users.js file inside our routes folder.

const express = require("express");
const router = express.Router();

const jwt = require('jsonwebtoken'); //use to create, verify, and decode tokens
const bcrypt = require('bcryptjs'); //use to hash passwords
const secret = require('../config').secret; //contains secret key used to sign tokens
const User = require("../models/User");

router.post("/register", (req, res) => {
})

router.post("/login", (req, res) => {
})

module.exports = router; //this should the last line of code

Here we are setting up for two routes, /register and /login. We then export our routes.js to make it accessible inside app.js.

Next, paste the following inside your config.js file.

module.exports = {
    dbUri: "mongodb://localhost/node-jwt",
    secret: "784sdsdsdhyohsd-098nwqjhu7324gcx64c847324gcx64cw5evr743c18448484809999999998",
}

Notice the dbUri property inside our modules.exports object? This is the connection string that we will use later for our database.

Now we'll set-up our database connection. Paste the following code inside db.js.

const dbUri = require("./config").dbUri;
const mongoose = require('mongoose');
mongoose.connect(dbUri, { useNewUrlParser: true, useUnifiedTopology: true });

Here we are retrieving the URI for our database from the config.js file.

We then require in mongoose (to interact with our database). Finally we connect to our database using the URI.

I am using a local connection for my mongoDb database. If you'd like to, you can create a live database here and connect to that one.

Now we will build our User model. Add the following your User.js file inside the models folder.

const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
    email: String,
    password: String
});

mongoose.model('User', UserSchema);
module.exports = mongoose.model('User');

We've created a simple schema. A schema defines the structure of the document. Each document inside our database collection will have an email and a password. We then use our schema to define a model, User. We will use the User model to create, and retrieve Users.

Start server

Now let's start our server to ensure everything is working properly.

Add "dev": "nodemon server.js", to your package.json file.

Next, run the command npm run dev to start your server.

You should see the message "listening on port 5000" printed to your terminal.

Register a User and create a token

We are now ready to start making using of JSON Web Tokens. We will register a User, create a token with the User ID as the payload, then we will return the token to our client.

I will be using Postman to test my API endpoints.

Update your /register endpoint in your user.js inside your routes folder to look like this.

router.post("/register", (req, res) => {
    const hashedPassword = bcrypt.hashSync(req.body.password, 10);

    User.create({
        email: req.body.email,
        password: hashedPassword,
    }).then((user) => {
        // create a token
        let token = jwt.sign({ id: user._id }, secret, {
            expiresIn: 86400 // expires in 24 hours
        })
        return res.status(201).send({ auth: true, token: token })
    })
        .catch((err) => {return res.send(err)})
})

First we use bcrypt to hash our password, because you should never save your passwords as plain text. We then create a User using the hashed password, and email.

We then create a token. The jwt.sign() method takes a payload and the secret key defined in config.js as parameters. It also takes another object which holds extra options. In this case the only option included is the expiresIn which tells the token to expire in 24 hours.

The token will be a unique string of characters. A part of this string represents the payload. In our case, the payload is an object containing only the id of the user.

If everything was successful we return an object to the client which contains the token.

Test our registration

Inside postman, we will make a post request to http://localhost:5000/register with the user information to register a new user.

Ensure that the HTTP method is set to POST. Click body, then select the x-www-form-urlencoded, next add the email and password as key-pair values. Click Send. You should recieve the following response.

Awesome! Our registration is working. We are receiving our token. We will use this token in subsequent requests. We can also use this token retrieve user information. Let's do that.

Add the following code to your user.js file inside your routes folder.

router.get('/current-user', function (req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token)
        return res.status(403).send({ auth: false, msg: 'No token provided.' });

    jwt.verify(token, secret, function (err, decoded) {
        if (err)
            return res.status(500).send({ auth: false, msg: 'Failed to authenticate token.' });

        User.findById(decoded.id, { password: 0 }, function (err, user) {
            if (err) return res.status(500).send("There was a problem finding the user.");
            if (!user) return res.status(404).send("No user found.");

            return res.status(200).send(user);
        });
    });
});

First we extract the token sent by the client. If there's no token, we return an appropriate message, and set auth to false.

If there's a token we verify it using the jwt.verify() method. We pass three parameters to this method, the token we want to verify, the secret key used to sign our tokens, and a callback function where we will handle the decoded token returned by jwt.verify(). We then use the decoded id to find our User inside our database using the User.findById() method. The { password: 0 } parameter signals the User.findById() to not return the password field. We then return the User to our client. We also handled any errors that may have occurred. Let us test this in postman.

Inside postman enter this url http://localhost:5000/user/current-user. Ensure that postman is set to GET. Next we must add the token to our request header. Click the Headers tab, enter Authorization as a key, then inside the Value field, type Bearer followed by your token (e.g Bearer token_goes_here). Press Send. If all went well, an object containing our user should be returned, else you'll receive one of the error responses.

We have completed registration and can use our token to get user details.

No we will implement login functionality for existing users.

We must have a way for existing users to log into our application. The login functionality is quite simple. Update your /login inside your user.js file, inside your routes folder to look like the following.

router.post('/login', function (req, res) {

    User.findOne({ email: req.body.email }, function (err, user) {
        if (err) return res.status(500).send('Error on the server.');
        if (!user) return res.status(404).send('Invalid Credentials');

        const passwordIsValid = bcrypt.compareSync(req.body.password, user.password);
        if (!passwordIsValid) return res.status(401).send({ auth: false, token: null, msg: 'Invalid Credentials' });

        const token = jwt.sign({ id: user._id }, secret, {
            expiresIn: 86400 // expires in 24 hours
        });

        res.status(200).send({ auth: true, token: token });
    });
});

First we check for a user, using the User.findOne() method, who's email matches the one submitted. If we found a user, we then compare the hash value of the submitted password, using the bcrypt.compareSync(), with the hashed password for the User found. If this password does not match then we send an appropriate response indication that invalid credentials were used, set auth to false, and set token to null. If the passwords match, we sign a new token, attach the User ID as a payload and return this token to the client, with auth set to true.

Let's test it in postman

Login with correct credentials

Awesome! It works as expected. Now what will happen if we submit an incorrect password?

attempt to login with incorrect credentials

Authorization

With authentication out of the way, we can now focus on authorization. Our authorization mechanism will be very simple.

We will create an endpoint /user/get-quote and we will ensure that only a user with a specific email can make a request to this endpoint and receive a quote. Paste the following inside your user.js route.

Note: you would not have such simple authorization in a real world application.

router.get("/get-quote", (req, res) => {
    const quote = "If a thing is humanly possible, consider it within your reach. - Marcus Aurelius";
    const acceptedEmail = "taslan@taslangraham.com.com"; //replace with email you registered with

    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token)
        return res.status(403).send({ auth: false, msg: 'No token provided.' });


    jwt.verify(token, secret, function (err, decoded) {
        if (err)
            return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });

        User.findById(decoded.id, { password: 0 }, function (err, user) {
            if (err) return res.status(500).send("There was a problem.");

            if (!user) return res.status(404).send("You must have an account to make this request.");
            if (user.email !== acceptedEmail) return res.status(401).send("You are not authorized.");

            return res.status(200).send(quote);
        });
    });
})

Here we verified and decoded a token. We then find a user using the id on the decoded token. We then check if this user's email is the same as our accepted email (acceptedEmail), then return the quote to the user.

Test it in postman.

What if we use a token for a different user?

Middlewares

Notice that the logic used to verify a token is the same as the logic inside the /current-user route handler? We can place this logic into what is known as a middleware.

Then we can use this middleware on any route that we wish to verify a token.

We can also place the logic to check if a user is authorized inside of a middleware as well.

First, what is a middleware

Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle. The next function is a function in the Express router which, when invoked, executes the middleware succeeding the current middleware.

Middleware functions can perform the following tasks:

  • Execute any code.
  • Make changes to the request and the response objects.
  • End the request-response cycle.
  • Call the next middleware in the stack.

If the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.

Paste the following code inside verifyToken.js in your middlewares folder.

const jwt = require('jsonwebtoken');
const secret = require('../config').secret;

const verifyToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token)
        return res.status(403).send({ auth: false, message: 'No token provided.' });

    jwt.verify(token, secret, function (err, decoded) {
        if (err)
            return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });

        req.userId = decoded.id;
        next();
    });
}
module.exports = verifyToken;

Add this line of code to the top of your user.js route file

const verifyToken = require("../middlewares/verifyToken");

Change your /current-user endpoint to the following.

router.get('/current-user', verifyToken, function (req, res, next) {
    User.findById(req.userId, { password: 0 }, function (err, user) {
        if (err) return res.status(500).send("There was a problem finding the user.");
        if (!user) return res.status(404).send("No user found.");

        res.status(200).send(user);
    });
});

Notice we used our verifyToken middleware. Also, our code is much simpler now.

Now test the /current-user route inside postman.

It still works

Now we will create our isAuthorized middleware. Paste the following inside your isAuthorized.js file.

const User = require("../models/User");

const isAuthorized = (req, res, next) => {
    const acceptedEmail = "taslan@taslangraham.com";

    User.findById(req.userId, { password: 0 }, function (err, user) {
        if (err) return res.status(500).send("There was a problem.");

        if (!user) return res.status(404).send("You must have an account to make this request.");
        if (user.email !== acceptedEmail) return res.status(401).send("You are not authorized.");
        req.email = user.email;
        next();
    });
}

module.exports = isAuthorized;

Now add this line of code at the top of your user.js route file.

const isAuthorized = require("../middlewares/isAuthorized");

We can now use the isAuthorized middleware. Change your /get-quote endpoint to look like this.

router.get("/get-quote", verifyToken, isAuthorized, (req, res) => {
    const quote = "If a thing is humanly possible, consider it within your reach. - Marcus Aurelius";
    return res.status(200).send(quote);
})

Notice we added both middlewares (verifyToken, and isAuthorized). You can combine middlewares. All our logic is now inside the isAuthorized middleware, making our code much cleaner. We can now test this in postman.

works perfectly

Recap

we've successfully implemented authentication, and authorization in our node.js application using JSON Web Tokens (JWT). We've also created middlewares to execute on our application’s request-response cycle.

Thank you for reading. If you caught an error, please let me know in the comments. Until next time, think, learn, create, repeat!

Sources

https://searchsecurity.techtarget.com/definition/authentication

https://techterms.com/definition/authentication

https://www.techopedia.com/definition/10237/authorization

https://jwt.io/introduction/

https://www.tutorialspoint.com/expressjs/expressjs_routing.htm

https://www.tutorialspoint.com/expressjs/expressjs_routing.htm

Buy Me A Coffee

Top comments (2)

Collapse
 
nomanmangalzai profile image
nomanmangalzai

You have explained so beautifully. I have been severely searching for a solution on how to implement token especially the way you have implemented it keeping it inside a function which will be used as a middlware.
Thank you very much.

Collapse
 
nomanmangalzai profile image
nomanmangalzai

Thank you for the nice explanation.