DEV Community

admirnisic
admirnisic

Posted on • Edited on

Authentification in NodeJS with Express using JWT

Github repository

I don't think it's necessary to explain why we need to have an authentication system in an application at all. You've probably heard of the terms authentication and authorization and I have to point out that these words have different meanings.
"Authentication is the act of validating that users are whom they claim to be. This is the first step in any security process. " Okta.com
"Authorization in system security is the process of giving the user permission to access a specific resource or function. This term is often used interchangeably with access control or client privilege." Okta.com

In this tutorial we will learn how to make an authentication system using JWT.

Database models

We will first have to deal with the database because we need to store user data somewhere. We need to store email and hashed password which will be used later for the sign in process. For this tutorial we will use NoSQL MongoDB database and we will also use mongoose. Mongoose is a MongoDB object modeling tool which is designed to work in an asynchronous environment and supports both promises and callbacks.

We will install the necessary packages:

npm install --save mongoose
npm install --save-dev @types/mongoose
Enter fullscreen mode Exit fullscreen mode

After the packages are installed, we can start making the model. We will create a model for the user who will have the fields _id, email, name and password. We will also create a unique email index so that there are no two users with the same email in our database.

import { model, Model, Schema } from 'mongoose';

export interface IUser {
  _id: string;
  email: string;
  password: string;
  name: string;
}

const IUserSchema = new Schema<IUser>(
  {
    _id: { type: String, required: true },
    email: {
      type: String,
      required: true,
      lowercase: true,
      index: true,
      unique: true,
    },
    name: { type: String, required: true },
    password: { type: String, required: true },
  },
  { collection: 'user', timestamps: true }
);

export const UserModel: Model<IUser> = model('user', IUserSchema);
Enter fullscreen mode Exit fullscreen mode

Now lets create a connection to the MongoDB database via mongoose.

Note: We need to have a MongoDB database running in order to connect to it. If you use docker you can find the docker-compose.yml file on github which link is provided in this tutorial and just run docker-compose up -d.

import mongoose, { Connection } from 'mongoose';

let mongooseConnection: Connection = null;
export async function connect(): Promise<void> {
  try {
    mongoose.connection.on('connecting', () => {
      console.log(`MongoDB: connecting.`);
    });
    mongoose.connection.on('connected', () => {
      console.log('MongoDB: connected.');
    });
    mongoose.connection.on('disconnecting', () => {
      console.log('MongoDB: disconnecting.');
    });
    mongoose.connection.on('disconnected', () => {
      console.log('MongoDB: disconnected.');
    });

    if (mongoose.connection.readyState !== 1 && mongoose.connection.readyState !== 2) {
      const conn = await mongoose.connect('mongodb://localhost:27017/ts-tutorial', { // <- replace connection string if necessary
        autoIndex: true,
        serverSelectionTimeoutMS: 5000,
      });
      mongooseConnection = conn.connection;
    }
  } catch (error) {
    console.log(`Error connecting to DB`, error);
  }
}

Enter fullscreen mode Exit fullscreen mode

Now in the server.ts file we can call the method for connecting to the database:

connect();
Enter fullscreen mode Exit fullscreen mode

If the application is successfully connected to the database then we should get the messages from log:

MongoDB: connecting.
Application started on port 3000!
MongoDB: connected
Enter fullscreen mode Exit fullscreen mode

Sign up process

We will first create an endpoint to which we will send data to create a new user. We will add the new route in the server.ts file. Email, name and password fields are required (we will not do the validation of parameters). After that, we must first check if there is an existing user with the same email and only after we determine that the user does not exist, can we proceed further.
The next step is to make a hash of the plain password because the plain password is never stored in the database. So when we create a new user we take his plain password, make a hash and keep the hash in the database. We will need the hashed password later for the sign in process.

Required npm packages:

npm install --save ulid
npm install --save bcrypt
npm install --save-dev @types/bcrypt
Enter fullscreen mode Exit fullscreen mode
app.post('/sign-up', async (req: Request, res: Response, next: NextFunction) => {
  const { email, name, password } = req.body;
  // check if user exists
  const userExists = await UserModel.findOne({ email: email });
  if (!!userExists) {
    next(new ErrorException(ErrorCode.DuplicateEntityError, { email }));
  }

  // generate password hash
  const hash = passwordHash(password);
  const newUser: IUser = {
    _id: ulid(),
    email,
    name,
    password: hash,
  };
  const created = await UserModel.create(newUser);
  res.send({ done: true });
});
Enter fullscreen mode Exit fullscreen mode

Note: Add the code in server.ts file before the routes for encoding the data that is being sent to our application from the client side.

const app = express();
app.use(
  express.urlencoded({
    extended: true,
  })
);

app.use(express.json());
Enter fullscreen mode Exit fullscreen mode

We used the bcrypt library to create a hash from a plain password. The code for hashing and comparing plain and hashed passwords:

import bcrypt from 'bcrypt';

export const passwordHash = (plainPassword: string): string => {
  const hash = bcrypt.hashSync(plainPassword, 10);
  return hash;
};

export const comparePassword = (plainPassword: string, passwordHash: string): boolean => {
  const compared = bcrypt.compareSync(plainPassword, passwordHash);
  return compared;
};
Enter fullscreen mode Exit fullscreen mode

In the code above, you can see that we have two functions. The passwordHash function will hash a plain password.
The comparePassword function will check that the plain password entered is the same as the hash from the database. We will need this method later for the login form.

If we have successfully created a user in the database, the next step is to create a JWT when the user tries to sign in.

Sign in process

As we said in the introduction, we will use the jsonwebtoken package and for that we need to install the packages:

npm install --save jsonwebtoken
npm install --save-dev @types/jsonwebtoken
Enter fullscreen mode Exit fullscreen mode

Actually how does it work? It is necessary to create a route for sign in where it will be necessary to enter email and password.

We will first check if there is a user with the provided email and if there is one, then we will take the password hash that is saved in the database. It is necessary to check whether the plain password from the login form agrees with the hash password from the database using the comparePassword method. If the method returns true then the user has entered a good password, otherwise the method will return false.

After that, it is necessary to generate jsonwebtoken through the mentioned library. We will generate the JWT with the help of a secret key which we keep in our application and the client should not be aware of the secret key. We will generate that jsonwebtoken string and return that token to the client application.

app.post('/sign-in', async (req: Request, res: Response, next: NextFunction) => {
  const { email, password } = req.body;
  // check if user exists
  const userExists = await UserModel.findOne({ email: email });
  if (!userExists) {
    next(new ErrorException(ErrorCode.Unauthenticated));
  }

  // validate the password
  const validPassword = comparePassword(password, userExists.password);
  if (!validPassword) {
    next(new ErrorException(ErrorCode.Unauthenticated));
  }

  // generate the token
  const token = generateAuthToken(userExists);

  res.send({ token });
});
Enter fullscreen mode Exit fullscreen mode

Code for JWT helper:

import { IUser } from '../models/db/user.db';
import jwt from 'jsonwebtoken';
import { ErrorException } from '../error-handler/error-exception';
import { ErrorCode } from '../error-handler/error-code';

const jwtKey = 'keyyyy';

export const generateAuthToken = (user: IUser): string => {
  const token = jwt.sign({ _id: user._id, email: user.email }, jwtKey, {
    expiresIn: '2h',
  });

  return token;
};

export const verifyToken = (token: string): { _id: string; email: string } => {
  try {
    const tokenData = jwt.verify(token, jwtKey);
    return tokenData as { _id: string; email: string };
  } catch (error) {
    throw new ErrorException(ErrorCode.Unauthenticated);
  }
};
Enter fullscreen mode Exit fullscreen mode

Authentication middleware

We will create one middleware called authMiddleware which we will put on the routes where we need to have protection and whose job will be to check if the JWT that was generated is valid. authMiddleware function is just a middleware function which will get a token from the header and check its validation. We can check the validation of the token with the function verifyToken which is placed inside our middleware.

The client side is required to send the JWT token string in the header for each API call that requires authentication. Header with authorization token looks like:

Authorization: Bearer eyJhbGciOiJIUzI1NiIXVCJ9TJV...r7E20RMHrHDcEfxjoYZgeFONFh7HgQ
Enter fullscreen mode Exit fullscreen mode

Protected route with middleware:

app.get('/protected-route', authMiddleware, (req: Request, res: Response, next: NextFunction) => {
  // data from the token that is verified
  const tokenData = req.body.tokenData;
  console.log('tokenData', tokenData);
  res.send('this is a protected route');
});
Enter fullscreen mode Exit fullscreen mode

The middleware itself:

import { Request, Response, NextFunction } from 'express';
import { ErrorCode } from '../error-handler/error-code';
import { ErrorException } from '../error-handler/error-exception';
import { verifyToken } from './jwt';

export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
  const auth = req.headers.authorization;
  if (auth && auth.startsWith('Bearer')) {
    const token = auth.slice(7);

    try {
      const tokenData = verifyToken(token);
      req.body.tokenData = tokenData;
      next();
    } catch (error) {
      throw new ErrorException(ErrorCode.Unauthenticated);
    }
  } else {
    throw new ErrorException(ErrorCode.Unauthenticated);
  }
};

Enter fullscreen mode Exit fullscreen mode

Wrapping up

In this tutorial we covered how to create basic models with mongoose and MongoDB and how to connect to MongoDB instances. We also learned how to create a new user and save the user in the database and what is important, how to create a hash password using the bcrypt library. After saving the user, we showed how to create a sign in process and generate a token using the jsonwebtoken library. Finally, we demonstrated how to create one middleware to be placed on a route to protect certain routes.

Top comments (0)