Server side user authentication and authorization in Node apps
By: Isabel Hong
Tags: Nodejs, Security, Authentication, Software Development, Programming

In web apps the process of user sign ups and subsequent logins is complex and there are several implementations to make your apps more robust and secure.

Each implementation adds a layer of abstraction and it is worth examining the various options developers have for authentication (verifying client identity) and authorization (granting permissions).

Technologies used

Express: Node JS framework, handles app routing

Node JS framework, handles app routing Express-validator: validates user credentials

validates user credentials Bcrypt: hashes passwords before saving them to our database

hashes passwords before saving them to our database Jsonwebtoken: generates tokens for authorization and access to our app routes

generates tokens for authorization and access to our app routes Mongoose: interacts with our MongoDB database

interacts with our MongoDB database Nodemon: restarts our server every time there is a change

restarts our server every time there is a change Dotenv: stores our environment variables

stores our environment variables Postman: to test our endpoints

//initialize a node project

npx express-generator npm install bcrypt dotenv express-validator jsonwebtoken mongoose npm install --save-dev nodemon

Set up your project

package.json

"scripts": {

"start": "node ./bin/www",

"dev": "nodemon ./bin/www"

},

Add these to your .gitignore file

node_modules/

.env

In your .env file set up your variables

MODE=dev

MONGO_DEV=mongodb://localhost:27017/auth-project

MONGO_PROD=mongodb://localhost:27017/auth-project-prod

JWT_SECRET=typearandomstringhere

Folder structure

App.js

Let’s require or import:

the dotenv package at the top of the file,

our Mongo database which we will include soon

the auth.route and modify the middleware for our routes in the express generator template.

Set up Mongo DB

Create a config folder and two files index.js and db.js. Your index.js file will contain the environment variables we stored in the .env file earlier.

In your db.js file set up your Mongo database using Mongoose

Run npm install and comment out the auth routes since we have not created them yet and it will throw an error.

// Comment out for now from app.js

line 12 const authRouter = require(‘./routes/auth’);

line 24 app.use(‘/api/auth’, authRouter);

In your terminal run mongo in a new window with the command according to your operating system and run the server in another window with the following command. You should see in your terminal “DB Connected mongodb://localhost:27017/auth-project’

npm run dev

Create a User model

const mongoose = require('mongoose');

const Schema = mongoose.Schema; const userSchema = new Schema({ name: {

type: String,

required: true,

}, email: {

type: String,

required: true,

}, password: {

type: String,

required: true,

}, createdOn: {

type: Date,

default: Date.now()

}, lastActive: {

type: Date,

default: Date.now()

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

Before the last module.exports line we will add a few more lines of code. In order to hash the password we need to use a Mongoose middleware and the third party package Bcrypt which uses a hashing algorithm.

Mongoose middlewares

There are many built-in middlewares in Mongoose and one of them is the .pre hook which allows us to call a function before, like saving a user to our database.

schema.pre('save', async function() {

await doStuff();

await doMoreStuff();

});

We can use it on the schema level, in our User model file. This middleware is executed before saving the user document and in our case we want to hash the password before saving it to our database as saving it in plaintext would expose it making it vulnerable for hackers.

We want to call the middleware and the first argument is ‘save’ and the second argument is a callback async function. Here it is important that we do not use arrow functions because we need to access the ‘this’ keyword inside the function. We use ‘this’ to access our user.

userSchema.pre('save', async function () { try {

const user = this;

user.password = await bcrypt.hash(user.password, saltRounds);}} catch (error) {

throw error;

}});

Hashing a password

To hash our password we are using Bcrypt. Hash functions (as opposed to encryption which is two-way) is a one way mathematical algorithm that makes it practically impossible to invert a hashed password to the original data.

Salting a password

Why do we needs salts? With hashing, in case more than one user has the same password, the hashed string output will be the same. In case one of the passwords is hacked, the hacker would have access to the other.

//Hashing example with no salt

//plaintext: password123 $2b$10$wAnm6x2BT1cOtzRULrBueeBsXIKsoz5mEmGieeQShOPc3K3JG.xwq

So to protect against this kind of scenario, we use “salts” before we hash a password. A salt is a unique random data that is appended to the password.

Bcrypt does this for us and we just need to define the saltRounds variable, for example const saltRounds =10. The number indicates the number of rounds the plaintext password goes through to be hashed. As the number of rounds increases so does the time for processing the password into the hashed format. In the documentation you can see the different scenarios and 10 rounds is a good standard considering the cost for your processor and the time your user has to wait.

rounds=8 : ~40 hashes/sec

rounds=9 : ~20 hashes/sec

rounds=10: ~10 hashes/sec

rounds=11: ~5 hashes/sec

rounds=12: 2-3 hashes/sec

rounds=13: ~1 sec/hash

rounds=14: ~1.5 sec/hash

rounds=15: ~3 sec/hash

rounds=25: ~1 hour/hash

rounds=31: 2-3 days/hash

Document.prototype.isModified()

One important line of code we added in line 29 is the .isModified function. The function returns true if the document is modified and we need to check this so that we don’t hash a password every time the user logs in but only when the password is modified.

Validating user data

Before we create our users and save them in our database we want to make sure that the data entered is in the proper format. Express validator is an Express middleware used for server side data validation. This is the basic boilerplate from the documentation.

const express = require('express');

const app = express();

const { body, validationResult } = require('express-validator');



app.use(express.json()); app.post('/user', [

// username must be an email

body('username').isEmail(),

// password must be at least 5 chars long

body('password').isLength({ min: 5 })

], (req, res) => {

// Finds the validation errors in this request and wraps them in an object with handy functions

const errors = validationResult(req);

if (!errors.isEmpty()) {

return res.status(400).json({ errors: errors.array() });

} User.create({

username: req.body.username,

password: req.body.password

}).then(user => res.json(user));

});

Create the register route with custom validators

However, when building an app for production, a better implementation can be to do the user validation including Regex and custom validators. First let’s create the user register route inside our routes/auth.js file.

Now we have used custom validators before creating and saving our user to the database.

Test /register endpoint in Postman and store a user

(Now that we have coded the auth.js file remember to uncomment the lines we commented before in app.js)

POST request: http://localhost:3000/api/auth/register

//body-raw-json {

"name":"test1",

"email":"test1@test.com",

"password": "12345abc",

"passwordConfirmation": "12345abc"

}

You should get a similar result in Postman

Create a user login route

Now that we have a user in our users collection database, let’s create the login route and here we need to go through the same custom validation as with the register route but need to write the following three lines of code — lines 5–7. We add Bcrypt’s to use its compare method to check if the password the user enters matches with the one in our database. In this process we will also provide an access token using JWT.

Test this POST request in postman http://localhost:3000/api/auth/login

"name":"test1",

"email":"test1@test.com",

"password": "12345abc",

"passwordConfirmation": "12345abc"

}

You should get the access token in the response

Access tokens and refresh tokens

One last important implementation requires us to add a few more lines of code in our login route for the refresh tokens.

The difference between access and refresh tokens is that the lifetime of refresh tokens is much longer than access tokens. For example we can set access tokens to be valid for 5 minutes while refresh tokens can be set for 6 months.

Access tokens grant access to a resource stored in the resource server while refresh tokens grant new access tokens whenever these expire. Access tokens usually have short life span which limits the time of a potential hacker to access the resources. The refresh tokens are issued by a separate authorization server.

Create a Refresh token model

Create a middleware to verify the token

Inside our middleware folder let’s write some code in our verifyToken.js file to authenticate users. We get the accessToken from the client request headers. Then we use the jwt.verify method. Check that the spelling is headers not header, otherwise it won’t work.

Create a private route for verified users

Create a new file users.js inside the routes folder to set up a private route for verified users. Here we are using verifyJWT as a middleware before we get our user’s info.

Implement refresh tokens in login route

This is our final auth.js file where we import the Refresh model and the crypto module to use its .randomBytes() method. This method generates a a random data that is cryptographically secure.

Let’s check this in Postman. Set this to GET and the url as http://localhost:3000/api/users/userInfo. In the body get the _id from the previous post request.

Now in the Headers tab create a new key called authorization and paste the value of the access token we got previously. You should get the user data as a response if everything works.

Conclusion

Having implemented this, it is worth to note that as developers we do have other options such as to use Passport an authentication middleware for Node or services like Auth0. However, it is good to learn the basics of how the processes work beforehand.