John Au-Yeung
Sep 12 · 6 min read

JavaScript is changing faster than ever. This means that existing apps will be using outdated JavaScript features quickly. It also means that you won’t get the convenience of using the latest features of JavaScript, like importing and exporting modules, async and await, generators, and many new features that might come in handy. Node.js runtime do not have all the latest features of JavaScript as of version 10. It is missing support for ES6 modules by default which is a big missing feature. It will be enabled in the future.

In this piece, we will build an app that uses JWT to store authentication data using the latest features of JavaScript. We can use it easily by using @babel-register and the babel-polyfill packages.

For the back end, we’ll use the Express framework, which runs on Node.js, and for the front end, we’ll use the Angular framework. Both have their own JWT add-ons. On the back end, we have the jsonwebtoken package for generating and verify the token.

On the front end, we have the @auth0/angular-jwt module for Angular. In our app, when the user enters user name and password and they are in our database, then a JWT will be generated from our secret key, returned to the user, and stored on the front-end app in local storage. Whenever the user needs to access authenticated routes on the back end, they’ll need the token.

There will be a function in the back-end app called middleware to check for a valid token. A valid token is one that is not expired and verifies as valid against our secret key. There will also be a sign-up and user credential settings pages, in addition to a login page.

Now we can start building the app. First, we create the front- and back-end app folders. Make one for each.

Then we start writing the back-end app. First, we install some packages and generate our Express skeleton code. We run npx express-generator to generate the code. Then we have to install some packages. We do that by running npm i @babel/register express-jwt sequelize bcrypt sequelize-cli dotenv jsonwebtoken body-parser cors . @babel/register allows us to use the latest JavaScript features.

express-jwt generates the JWT and verifies it against a secret key.bcryptdoes the hashing and salting of our passwords. sequelize is our ORM for doing CRUD. cors allows our Angular app to communicate with our back end by allowing cross-domain communication. dotenv allows us to store environment variables in an .env file. body-parser is needed for Express to parse JSON requests.

Then we make our database migrations. First, we run npx sequelize-cli init to generate skeleton code for our database-to-object mapping. Then we run:

npx sequelize-cli model:generate --name User --attributes username:string, password:string, email:string

We make another migration and put:

'use strict';module.exports = {
up: (queryInterface, Sequelize) => {
return Promise.all([
queryInterface.addConstraint(
"Users",
["email"],
{
type: "unique",
name: 'emailUnique'
}),queryInterface.addConstraint(
"Users",
["userName"],
{
type: "unique",
name: 'userNameUnique'
}),
},
down: (queryInterface, Sequelize) => {
return Promise.all([
queryInterface.removeConstraint(
"Users",
'emailUnique'
),queryInterface.removeConstraint(
"Users",
'userNameUnique'
),
])
}
};

This makes sure we don’t have two entries with the same username or email.

This creates the User model and will create the Users table once we run npx sequelize-cli db:migrate .

Then we write some code. First, we put the following in app.js :

require("@babel/register");
require("babel-polyfill");
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const user = require('./controllers/userController');
const app = express();app.use(cors())
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use((req, res, next) => {
res.locals.session = req.session;
next();
});
app.use('/user', user);app.get('*', (req, res) => {
res.redirect('/home');
});
app.listen((process.env.PORT || 8080), () => {
console.log('App running on port 8080!');
});

We need:

require("@babel/register");
require("babel-polyfill");

to use the latest features in JavaScript. We cannot use Babel features in this file, but we can use it in other files that this file references, and in any files that reference those files.

And we need:

require('dotenv').config();

to read our config in an .env file.

This is the entry point. We will create userController in the controllersfolder shortly.

app.use(‘/user’, user); routes any URL beginning with user to the userController file.

Next, we add the userController.js file:

const express = require('express');
const bcrypt = require('bcrypt');
const router = express.Router();
const models = require('../models');
const jwt = require('jsonwebtoken');
import { saltRounds } from '../exports';
import { authCheck } from '../middlewares/authCheck';
router.post('/login', async (req, res) => {
const secret = process.env.JWT_SECRET;
const userName = req.body.userName;
const password = req.body.password;
if (!userName || !password) {
return res.send({
error: 'User name and password required'
})
}
const users = await models.User.findAll({
where: {
userName
}
})
const user = users[0];
if (!user) {
res.status(401);
return res.send({
error: 'Invalid username or password'
});
}
try {
const compareRes = await bcrypt.compare(password, user.hashedPassword);
if (compareRes) {
const token = jwt.sign(
{
data: {
userName,
userId: user.id
}
},
secret,
{ expiresIn: 60 * 60 }
);
return res.send({ token });
}
else {
res.status(401);
return res.send({
error: 'Invalid username or password'
});
}
}
catch (ex) {
logger.error(ex);
res.status(401);
return res.send({
error: 'Invalid username or password'
});
}});
router.post('/signup', async (req, res) => {
const userName = req.body.userName;
const email = req.body.email;
const password = req.body.password;
try {
const hashedPassword = await bcrypt.hash(password, saltRounds)
await models.User.create({
userName,
email,
hashedPassword
})
return res.send({ message: 'User created' });
}
catch (ex) {
logger.error(ex);
res.status(400);
return res.send({ error: ex });
}
});
router.put('/updateUser', authCheck, async (req, res) => {
const userName = req.body.userName;
const email = req.body.email;
const token = req.headers.authorization;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
try {
await models.User.update({
userName,
email
}, {
where: {
id: userId
}
})
return res.send({ message: 'User created' });
}
catch (ex) {
logger.error(ex);
res.status(400);
return res.send({ error: ex });
}});router.put('/updatePassword', authCheck, async (req, res) => {
const token = req.headers.authorization;
const password = req.body.password;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
try {
const hashedPassword = await bcrypt.hash(password, saltRounds)
await models.User.update({
hashedPassword
}, {
where: {
id: userId
}
})
return res.send({ message: 'User created' });
}
catch (ex) {
logger.error(ex);
res.status(400);
return res.send({ error: ex });
}});
module.exports = router;

The login route searches for the User entry. If it’s found, it then checks for the hashed password with the compare function of bcrypt. If both are successful, then a JWT is generated. The signup route gets the JSON payload of username and password and saves it.

Note that there is hashing and salting on the password before saving. Passwords should not be stored as plain text.

This first is the plain text password, and the second is a number of salt rounds.

updatePassword route is an authenticated route. It checks for the token, and if it’s valid, it will continue to save the user’s password by searching for the User with the user ID from the decoded token.

We will add the authCheck middleware next. We create a middlewares folder and create authCheck.js inside it.

const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET;
export const authCheck = (req, res, next) => {
if (req.headers.authorization) {
const token = req.headers.authorization;
jwt.verify(token, secret, (err, decoded) => {
if (err) {
res.send(401);
}
else {
next();
}
});
}
else {
res.send(401);
}
}

You should use the same process.env.JWT_SECRET for generating and verifying the token. Otherwise, verification will fail. The secret shouldn’t be shared anywhere and shouldn’t be checked in to version control.

This allows us to check for authentication in authenticated routes without repeating code. We place it in between the URL and our main route code in each authenticated route by importing and referencing it.

We make an .env file of the root of the back-end app folder, with the following content. (This shouldn’t be checked in to version control.)

DB_HOST='localhost'
DB_NAME='login-app'
DB_USERNAME='db-username'
DB_PASSWORD='db-password'
JWT_SECRET='secret'

The back-end app is now complete. Now can we can use a front-end app, mobile app, or any HTTP client to sign in.

The Startup

Medium's largest active publication, followed by +504K people. Follow to join our community.

John Au-Yeung

Written by

Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

The Startup

Medium's largest active publication, followed by +504K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade