JWT Authentication in Typescript with Express
Tags: typescript, node, express, jwt
Published: 2022-04-21 10:15:43

I started my journey to the world of authentication in Typescript with the need to authenticate users on the front and the backend of the application.

The magic recipe for this was:

**Backend part:**
- 2 post routes for user authentication (for sign up and login)
- 1 controller, 1 service
- 1 model, 1 collection in MongoDb
- 1 bcrypt package for hashing and comparing passwords (+ its types)
- 1 JWT package for creating and verification of tokens (+ its types)
- 1 middleware for authentication

**Frontend part:**

- Getting a token from the backend and storing it
- Getting a token from storage and putting in headers

We have a plan, so let’s start our journey!

## Step 1. Creating routes, controllers, services, models

This project was built according to MVC pattern, such a structure was created for logic division.

Routes
```ts
import * as userController from '../controllers/user.controller';
 
Router.post('/login', userController.loginOne);
Router.post('/register', userController.registerOne);
```

Controller
```ts
import { Request, Response } from 'express';
import { getErrorMessage } from '../utils/errors.util';
import * as userServices from '../services/user.service';
import { CustomRequest } from '../middleware/auth';
 
export const loginOne = async (req: Request, res: Response) => {
 try {
   const foundUser = await userServices.login(req.body);
   res.status(200).send(foundUser);
 } catch (error) {
   return res.status(500).send(getErrorMessage(error));
 }
};
 
export const registerOne = async (req: Request, res: Response) => {
 try {
   await userServices.register(req.body);
   res.status(200).send('Inserted successfully');
 } catch (error) {
   return res.status(500).send(getErrorMessage(error));
 }
};

```

Function getErrorMessage from utils folder includes: 
```ts
export function getErrorMessage(error: unknown) {
 if (error instanceof Error) return error.message;
 return String(error);
}
```

Service
```ts
import { DocumentDefinition } from 'mongoose';
import UserModel, { I_UserDocument } from '../models/user.model';
 
export async function register(user: DocumentDefinition<I_UserDocument>): Promise<void> {
 try {
   await UserModel.create(user);
 } catch (error) {
   throw error;
 }
}
 
export async function login(user: DocumentDefinition<I_UserDocument>) {
 try {
   const foundUser = await UserModel.findOne({ name: user.name, password: user.password });
 } catch (error) {
   throw error;
 }
}
```
Model
```ts
import mongoose from 'mongoose';
 
export interface I_UserDocument extends mongoose.Document {
 name: string;
 password: string;
}
 
const UserSchema: mongoose.Schema<I_UserDocument> = new mongoose.Schema({
 name: { type: String, unique: true },
 password: { type: String },
});
 
const UserModel = mongoose.model<I_UserDocument>('User', UserSchema);
```
I didn’t include _id in interface I_UserDocument because he extends mongoose.Document and already includes _id.

Use Postman to check the results.

## Step 2: Hashing passwords

Hashing is different from encrypting in that it is a one-way action: we get the password and salt to it and get a line of letters, numbers and symbols.

The crucial difference is that there is no way to get the initial password. So each time that user sets his password this password will be hashed the same way and hashed result will be the same.

Example of hashed password: `$2b$08$LSAG/cRp.tSlvTWzp1pwoe50bDWEDjLfK7Psy5ORzf4C.PxJYZeau`

**While this step we keep in mind 2 aims:**

1. Hash the password right after the signing up
2. While logging in check if the hashed version of the password is the same as stored in Mongo

Install Bcrypt and its types: 
```bash
npm i bcrypt @types/bcrypt
```

**Hashing the password while signing up**

Here we use the option of schema to use middleware. We check the password and change it bcrypt and its salt. 

A plain password is hashed with salt (a random string) that has an unpredictable result. The salt gets automatically included with the hash, so you do not need to store it in a database.

In this case, number 8 means salt rounds,  the minimum that is recommended is 8.

Model
```ts
import mongoose from 'mongoose';
import bcrypt from 'bcrypt';
 
const saltRounds = 8
 
UserSchema.pre('save', async function (next) {
 const user = this;
 if (user.isModified('password')) {
   user.password = await bcrypt.hash(user.password, saltRounds);
 }
 next();
});
```

As a result of using middleware on the model we hash the password and store it hashed in the database.

**Comparing the received password and hashed one**

Service
```ts
export async function login(user: DocumentDefinition<I_UserDocument>) {
 try {
   const foundUser = await UserModel.findOne({ name: user.name });
 
   if (!foundUser) {
     throw new Error('Name of user is not correct');
   }
 
   const isMatch = bcrypt.compareSync(user.password, foundUser.password);
 
   if (isMatch) {
return foundUser 
   } else {
     throw new Error('Password is not correct');
   }
 } catch (error) {
   throw error;
 }
}
```
We search user by name and if the user with such name exists in the database we start comparing the received password from the user and the hashed password stored in the database with: **bcrypt.compareSync(password-from-user, password-from-database)**
If 2 passwords are the same we return the user.

## Step 3. Tokens Implementation

Our aims for this step:

1. Create a token while logging in
2. Verify token while logging
3. Send token to the frontend

What is it token?

It is a safe means that includes header, payload and signature.

How a token may look like?
`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.vaYmi2wAFIP-RGn6jvfY_MUYwghZd8rZzeDeZ4xiQmk`

The first part before the dot is a header that includes the **algorithm and token type**. The second is **a payload** that includes all data you want to set into a token and timestamps that indicate token’s expiration time
The third is **a signature** that you choose by yourself.

You can check your JWT on [https://jwt.io/](https://jwt.io/)

An example of a decoded token:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1cgq42f9lshzqnp4r31m.png)

Important! We don’t need to store JWT tokens in the database.

**Creating tokens while logging in**

Install JWT and its types: 
```bash
npm i jsonwebtoken @types/jsonwebt
```

Service
```ts
if (isMatch) {
     const token = jwt.sign({ _id: foundUser._id?.toString(), name: foundUser.name }, SECRET_KEY, {
       expiresIn: '2 days',
     });
 
     return { user: { _id, name }, token: token };
   } else {
     throw new Error('Password is not correct');
   }
```
In the part with isMatch conditions  I created a token and return it with the user.
In the token’s payload I put the user’s id and user’s name and didn’t send also the password. SECRET_KEY is a plain text that also is my personal signature for token that I imported.

**Verifying (decoding) tokens**

For this we need middleware that happens between controllers and service.
I created file auth.ts in folder middleware.

```ts
import jwt, { Secret, JwtPayload } from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
 
export const SECRET_KEY: Secret = 'your-secret-key-here';
 
export interface CustomRequest extends Request {
 token: string | JwtPayload;
}
 
export const auth = async (req: Request, res: Response, next: NextFunction) => {
 try {
   const token = req.header('Authorization')?.replace('Bearer ', '');
 
   if (!token) {
     throw new Error();
   }
 
   const decoded = jwt.verify(token, SECRET_KEY);
   (req as CustomRequest).token = decoded;
 
   next();
 } catch (err) {
   res.status(401).send('Please authenticate');
 }
};
```
We get a token from a header by deleting “Bearer “ from the string, decode the token and add to the user decoded (signatured) token.

So we come back to controllers to function LoginOne

Controllers
```ts
export const loginOne = async (req: Request, res: Response) => {
 try {
   const foundUser = await userServices.login(req.body);
   //console.log('found user', foundUser.token);
   res.status(200).send(foundUser);
 } catch (error) {
   return res.status(500).send(getErrorMessage(error));
 }
};
```
Now due to the middleware, we get not only the user but also the user.token (with signatured token).

Important!
We import auth and set it on all routes that we want to be authenticated.
2 routes that couldn’t be authenticated they are routes for signing in and signing up.

An example of another roots with required authentication:

```ts
Router.get('/all', auth, searchController.getAll);
Router.post('/', auth, searchController.addOne);
Router.delete('/:id', auth, searchController.deleteOne);
```

We finished with JWT Authentication on the backend so let’s move to the front.

## Step 4. Move to front

Our steps on the frontend:

1. Get a token from the backend
2. Store a token
3. Extract token from storage and add it to the header for chosen axios requests (excluding signing up and signing)
4. Change UI

We won’t go throw all steps in detail, I will give only a general description how it can be implemented.

On the frontend I used React.js and axios package.

**Get a token from the backend with axios request - done :)**

**Storing the token**
Options for storing : 

1. Global State (Redux, Context)
2. Cookies
3. Local or session storage

**Getting the token from storage and puting it in the Header**

I stored the token in cookies so I created and imported the function that gets the token from cookies. I_AuthHeader is a custom interface.

```ts
export const authHeader = (): I_AuthHeader => {
 const token = getTokenFromCookies();
 return {
   headers: {
     Authorization: "Bearer " + token,
   },
 };
};

```
An example of adding header

```ts
import axios from "axios";
let baseUrl = "http://localhost:8080/";
const ApiHeader = axios.create({
 baseURL: baseUrl,
});
```

```ts
export const getSearchWords = async (): Promise<I_Search[]> => {
 try {
   const { data } = await ApiHeader.get("api/search/all", authHeader());
   return data;
 } catch (error) {
   console.error(error);
   throw error;
 }
};
```
**Enjoy improving UI !**

I would love to get your feedback in comments :)