In this tutorial I am going to explain how to create a user registration system from scratch using JWT which stands for JSON Web Tokens.


Table of Contents:


User Registration

In this section of the tutorial we'll build the user registration form which users can interact and give us their details to store in our database.

User Registration Form

First, in the app.js we are going to add the new signup route. As you can see in line 6 and line 18 we are defining the new route.

const express = require('express');
const hbs = require('hbs');
const app = express();
const indexRouter = require('./routes/index');
const loginRouter = require('./routes/login');
const signupRouter = require('./routes/signup');

// view engine setup
app.set('view engine', 'hbs');
// Set handlebars partials folder
hbs.registerPartials(__dirname + '/views/partials');

// Middleware
app.use(express.urlencoded({ extended: true }));

// Routes
app.use('/login', loginRouter);
app.use('/signup', signupRouter);
app.use('/', indexRouter);

const port = 3000;
app.listen(port, () => console.log(`Server listening at: ${port}!`));

Next step is to actually create the route. This is very similar (at least for now) with the login route. We have 3 fields, email, username and password.

const express = require('express');
const router = express.Router();
const { check, validationResult } = require('express-validator/check')

/* GET signup page. */
router.get('/', (req, res, next) => {
    return res.render('signup', { title: 'PicoBlog' });
});

const validationRules = [
    check('email').isEmail(),
    check('password').isLength({ min: 6 }),
    check('username').isAlphanumeric(),
]

/* POST signup page. */
router.post('/', validationRules, (req, res, next) => {
    console.log(req.body)
    console.log((validationResult(req).array()))
    return res.render('signup', { title: 'PicoBlog' });;
});

module.exports = router;

For now we just console log the results of the post request, but, at a later stage we will save the users that are signing up to our database. Last, but not least let's create the presentation layer which is the sign up form.

<html>

<head>
    <title>{{title}}</title>
</head>

<body>
    {{> navigation}}
    <h1>Sign Up</h1>

    <form action="/signup" method="post">
        <label>Name:</label>
        <input type="text" name="name" required />
        <label>Surname:</label>
        <input type="text" name="surname" required />

        <label>Email:</label>
        <input type="email" name="email" required />

        <label>Username:</label>
        <input type="text" name="username" required />

        <label>Password:</label>
        <input type="password" name="password" required />

        <button type="submit">Sign Up</button>
    </form>
</body>

</html>

Again, really similar to the login page. It just has some additional fields that they will make our app personalized to each user.

Hopefully, you will be now able to see the screenshot below when navigating to /signup page.

The structure of the project will now look as below.

microblog/
	knex/
		migrations/
			20190325211227_create_users_table.js
			20190401214024_create_posts_table.js
		knex.js
	node_modules/
	routes/
		index.js
		login.js
        signup.js
	views/
		partials/
			navigation.hbs
		index.hbs
        login.hbs
        signup.hbs
	app.js
	knexfile.js
	package.json
	package-lock.json

User Registration Backend

In this section of the tutorial we will learn how to access the database and save the user's information. A really important detail here is how to securely store passwords in our database. We will look at concepts such as salt and hashing and also how to organize the code in a way that we will be able to easily extend it in the future.

In the root directory of the project I created another directory called repositories. In this new directory we will have a file called userRepositoty.js which it will be resposnible for writing the data when a user is registered to the Postgres database.

Let's see how the userRepository.js looks like.

const bcrypt = require('bcrypt');
const knex = require('../knex/knex');

const save = async (user) => {
    const { email, name, password, surname } = user;

    return await knex('users')
        .insert({
            createdAt: new Date(),
            email,
            name,
            password: await hashPassword(password),
            surname
        })
        .returning('*')
        .then((res) => res)
        .catch((err) => { console.log(err); throw err });
}

const hashPassword = async (plaintextPassword) => {
    const saltRounds = 10;
    const salt = await bcrypt.genSalt(saltRounds);
    return await bcrypt.hash(plaintextPassword, salt);
}

module.exports = { save }

Line 1: bcrypt is a package that we will need in order to salt and hash users` passwords.

Line 2: We import the knex configuration that we created earlier in chapter 4.

Line 4 - 18: This is the save(user) function that we will use in the route of our app. This function is saving into the database the user from the sign up page. Here we are creating a query that inserts into the database the user's details and a hashed password. Also, on line 15 we are specifying to knex to return everything that the row we just added to the Postgres database has. Even fields that we did not initialized such as createdAt and deletedAt. In line 16, we are executing the query and by using an arrow function we return the res back. Last, but not least on line 17 we are catching every error that might occur, logging it and then throw it again. In a future post we will see how we can handle all these potential errors.

Line 20 - 24: Here is the hashPassword function that, given the password in plaintext, is creating a hash so it can be stored safely in our database. Even if the website is hacked, at least the hackers will not be able to retrieve the plaintext password.

Line 26: We export the save function so it can be used in other modules of our application.

By mocking a user object we can test that the function is actually saving the user object in the database. Logging the results in the terminal looks like this

➜  picoblog git:(master) ✗ node repositories/userRepository.js
[ { id: 33,
    createdAt: 2019-04-16T20:43:49.466Z,
    updatedAt: null,
    deletedAt: null,
    name: 'jose',
    surname: 'armando',
    email: 'aa1@gmail.com',
    password:
     '$2b$10$k6n2GXneYR4CAK2tFE3QUOwc7lLeLV4noUwHjmGb3rYczKeomWfNa' } ]
     ```

The password that I used is the text 12 but because it is hashed it looks completely different. If you want to learn more about hash and salt you can read this article from Auth0, which explains in great detail why this is needed.

Knex Migrations

An unfortunate mistake and how to fix it. Earlier I forgot to add the username column in the users table. To fix it we have to create a new migration and add the missing column. To do that, first we have to create a new migration file

knex migrate:make add_column_username_table_users knex/migrations

The migrations file for adding a new column looks like this

exports.up = (knex, Promise) => {
    return knex.schema.alterTable('users', (t) => {
        t.string('username').unique().notNull();
    });
};

exports.down = (knex, Promise) => {
    return knex.schema.table('users', (t) => {
        t.dropColumn('username');
    });
};

To run the latest migration we have to do.

Note: Make sure that you don't have any users in the database, because if you do this migration will fail.

knex migrate:latest

After running the migration we can see that the new column is added to our database. Yeaaah!

Last, to connect everything together, inside the signup router we have to call the repository to save the new user.

await userRepository.save(req.body);

We have to await the operation because is async. If you want to understand the javascript async/await model you can read this article from Alligator.io

The updated signup.js router looks like this

const express = require('express');
const router = express.Router();
const { check, validationResult } = require('express-validator/check')

const userRepository = require('../repositories/userRepository');

/* GET signup page. */
router.get('/', (req, res, next) => {
    return res.render('signup', { title: 'PicoBlog' });
});

const validationRules = [
    check('email').isEmail(),
    check('password').isLength({ min: 6 }),
    check('username').isAlphanumeric(),
]

/* POST signup page. */
router.post('/', validationRules, async (req, res, next) => {
    console.log(req.body)
    console.log((validationResult(req).array()))

    if (validationResult(req).array().length !== 0) {
        return res.status(500);
    }

    await userRepository.save(req.body);
    return res.render('signup', { title: 'PicoBlog' });;
});

module.exports = router;

Let's explain the changes line by line

Line 5: Import the userRepository that we created earlier which has the responsibility to save users to the database.

Line 23 - 25: Logic to return an error when the validation returns an error.

Line 27: Calling the userRepository to actually save the user.

User Login

In this part of the tutorial we are going to demonstrate how the login system should work. Let's break it down into steps to understand the flow and how things are working.

When the user clicks on the login button a request to the /login route is made. Then based on the user's credentials, which are the username and password we should check in the database and fetch the details for that user. If it does exist and the password matches then the user gets a token. The token then can be used for making requests to parts of the website that users that are not logged in are not allowed. One small detail here is that in the database we are not saving the clear text password of our users. So, we will make the same process of salt and hashing and then compare the 2 passwords to see if the hashes are matching. If the hashed passwords are matching then it means that the user entered the correct password and we should give her back the token.

Let's dive into code and see how to implement this.

routes/login.js

/* POST login page. */
router.post('/', validationRules, async (req, res, next) => {
    const user = await userRepository.getUserByUsername(req.body.username);
    
    const isPasswordCorrect = await bcrypt.compare(req.body.password, user.password);
    if (isPasswordCorrect) {
        console.log('Password is correct');
    } else {
        console.log('Password is wrong');
    }

    return res.render('login', { title: 'PicoBlog' });;
});

Line 3: We are asking from the repository (which is connected to the database) to return a user that has the username that was posted to this route. The username is passed from the user when she presses the login button.

Line 5: If the user exists then the database will return a row and therefore the repository will also return what we need (the user information). After that, using the npm package called bcrypt, we can compare the clear text password from the request body to the hashed password that we store in the database when the user signed up. If the password is correct we just logged a message to the console, at least for now.

Let's continue and implement the logic of returning a token if the user has logged in successfully.

For that we are going to need a library that creates tokens. A really popular one in the JavaScript community is jsonWebToken. We can install it in our project by typing in our terminal the following

npm i jsonwebtoken

The code looks like this

const bcrypt = require('bcrypt');
const express = require('express');
const jwt = require('jsonwebtoken');
const router = express.Router();
const { check, validationResult } = require('express-validator/check')


const userRepository = require('../repositories/userRepository');

/* GET login page. */
router.get('/', (req, res, next) => {
    return res.render('login', { title: 'PicoBlog' });
});

const validationRules = [
    check('password').isLength({ min: 6 }),
    check('username').isAlphanumeric(),
]

/* POST login page. */
router.post('/', validationRules, async (req, res, next) => {
    const user = await userRepository.getUserByUsername(req.body.username);

    const isPasswordCorrect = await bcrypt.compare(req.body.password, user.password);
    if (isPasswordCorrect) {
        console.log('Password is correct');
        const token = jwt.sign({ user }, 'secretKey');
        return res.render('login', { title: 'PicoBlog', token });;
    } else {
        console.log('Password is wrong');
    }

    return res.render('login', { title: 'PicoBlog' });;
});

module.exports = router;

The only thing that really changed from the previous example is that now, we are using the library to encode the user details and send them back to the frontend of our application. We can encode and send back anything but is a common practice to send the user details so we can add personalization in the frontend. At this point I should mentioned that the token is not encrypted. It is just signed with the private key that only the server knows. So, in the frontend or using services like jwt.io we can decode it and observe what the token has. The server however, can check that the token was signed using the server's private key and it can verify that the token is trusted. No other service can sign a token without having the server's private key. This is why no one else can create a fake token and impersonate a different user.

Line 27: Here we encode the user details.

Line 28: In the response we are including the JWT.

The last piece of the puzzle is to have a bit of frontend code to save the token to the user's browser. Then even if the user navigates away from our website the token will be stored there so they will not have to login again and again every time they come to the website.

A small caveat

When making POST requests from the frontend, the browser for security reasons implements CORS. CORS stands for Cross-Origin Resource Sharing. There are multiple things that you have to take into account when you have to make a cross origin request. You can learn more about CORS here.

Here we are not making a request to a different domain but because we are changing the headers of the request therefore, the browser is making a preflight check to see if the server will accept the request before actually sending the POST request. If we open the network tab of the browser we can see the 2 requests.

To make the server accept this type of requests we can use a library that is responsible for adding a CORS middleware or we can create a specific route that accept the request type of OPTIONS. I chose the 2nd option.

Let's see how the code looks.

/* Options route used for preflight request to the login POST route (cors) */
router.options("/*", (req, res, next) => {
    res.header('access-control-allow-origin', '*');
    res.header('access-control-allow-methods', 'POST');
    res.header('access-control-allow-headers', ' Accept, access-control-allow-origin, Content-Type');
    res.sendStatus(204);
});

We specify some of the headers that the browser expects to get back and the status 204 which means No Content but the requests has been made successfully.

The final code of the login route

const bcrypt = require('bcrypt');
const express = require('express');
const jwt = require('jsonwebtoken');
const router = express.Router();
const { check, validationResult } = require('express-validator/check')


const userRepository = require('../repositories/userRepository');

/* GET login page. */
router.get('/', (req, res, next) => {
    return res.render('login', { title: 'PicoBlog' });
});

/* Options route used for preflight request to the login POST route (cors) */
router.options("/*", (req, res, next) => {
    res.header('access-control-allow-origin', '*');
    res.header('access-control-allow-methods', 'POST');
    res.header('access-control-allow-headers', ' Accept, access-control-allow-origin, Content-Type');
    res.sendStatus(204);
});

const validationRules = [
    check('password').isLength({ min: 6 }),
    check('username').isAlphanumeric(),
]

/* POST login page. */
router.post('/', validationRules, async (req, res, next) => {
    console.log(req.body);
    console.log((validationResult(req).array()));
    res.header('access-control-allow-origin', '*');
    const user = await userRepository.getUserByUsername(req.body.username);
    console.log(user)

    const isPasswordCorrect = await bcrypt.compare(req.body.password, user.password);
    if (isPasswordCorrect) {
        const token = jwt.sign({ user }, 'secretKey');
        return res.send(JSON.stringify({ authorization: token }));
    }
    return res.sendStatus(500);;
});

module.exports = router;

The only change is that we added the header

res.header('access-control-allow-origin', '*');

to the response of the post request because the browser expects that. In the future we can also limit the acceptable origins to just the origin of the domain that we will have for this project. This will eliminate other websites / domains of making valid requests to our server.

Saving JWT to localStorage

In the final chapter, we will see how to save the token that we are getting back from the server to localStorage so users that are logged in can make requests to protected part of the website.

Under the public folder I created a file called authentication.js with the following  code.

"use strict";

const getToken = async () => {
    let url = 'http://127.0.0.1:3000/login';
    let data = {
        username: document.getElementById('username').value,
        password: document.getElementById('password').value
    };

    return fetch(url, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-type': 'application/json',
            'access-control-allow-origin': '*',
        },
        body: JSON.stringify(data),
    });
}

const login = async () => {
    const res = await getToken();
    if (res.status === 200) {
        const token = await res.json();
        localStorage.setItem('Authorization', token.authorization);
        window.location.href = '/';
    } else {
        document.getElementById('password-errors').style.visibility = "visible";
    }
}

The getToken() arrow function is making a post request with a body of username and password that it gets from the html login form.

The login() function is called when we press the button in the login form and it checks if the request return a http status code of 200. If not it's changing a css class to display an error message to the user.


Congratulations, you have completed chapter 5. On this chapter we finished the Signup and Login system. We also learn how to create and store passwords in our backend securely and how to create JWT tokens and how to store them in the users' browser.

You can find the final result of this chapter in Github.

If you liked the tutorial, please consider subscribing to my blog. That way I get to know that my work is valuable to you and also notify you for future tutorials. Stay tuned!