With single-page front-end apps and mobile apps being more popular than ever, the front end is decoupled from the back end. Since almost all web apps need authentication, there needs to be a way for front-end or mobile apps to store user identity data in a secure fashion.
JSON Web Tokens (JWT) is one of the most common ways to store authentication data on front-end apps. With Node.js, there are popular libraries that can generate and verify the JWT by checking for its authenticity. They do this by checking against a secret key stored in the back end and also by checking for an expiry date.
The token is encoded in a standard format that’s understood by most apps. It usually contains user identity data like user ID, user name, etc. It’s given to the user when the user successfully completes authentication.
In this piece, we will build an app that uses JWT to store authentication data.
Overview
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.
Building the App
With this plan, we can begin.
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.bcrypt
does 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](http://twitter.com/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.
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 controllers
folder 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.