TypeScript Express

Registering users and authenticating with JWT

Marcin Wanago
ExpressJavaScript

Today we cover an essential part of almost every application: registering users and authenticating them. To implement it, we use JSON Web Tokens (JWT). Instead of getting help from libraries like Passport, we build everything from the ground up to get the best understanding of how it works. As always, all of the code is available in the express-typescript repository. Feel free to give it a star if you find it helpful.

Registration

To start things up we create the User interface and model.

src/users/user.interface.ts
1interface User {
2  name: string;
3  email: string;
4  password: string;
5}
6 
7export default User;
src/users/user.model.ts
1import * as mongoose from 'mongoose';
2import User from './user.interface';
3 
4const userSchema = new mongoose.Schema({
5  name: String,
6  email: String,
7  password: String,
8});
9 
10const userModel = mongoose.model<User & mongoose.Document>('User', userSchema);
11 
12export default userModel;

Hashing

A catch here is that we don’t want to save the passwords in plain text! Imagine your database getting breached and all the passwords leaking out. Not good!

The purpose of a hashing algorithm is to turn one string into another string. If you change just one character in a string, the hash is entirely different. The most important thing is that it is a one-way operation: it can’t be reversed easily. When the user attempts to log in, you can hash his password again and compare with the one saved in the database.

Hashing the same string twice gives the same result. To prevent users that have the same password from having the same hash, we use salt. It is a random string that is added to the original password to achieve a different result each time. It should be different for each password.

Bcrypt

In this article, we use a bcrypt hashing algorithm implemented by the bcrypt npm package. It takes care of hashing the strings, comparing plain text strings with hashes and appending salt. Using it we define salt rounds. It is basically a cost factor: it controls the time needed to receive an output hash. Increasing the cost factor by one doubles the time. The more significant the cost factor, the more difficult is reversing the hash by brute-forcing. The salt that’s used for hashing someone’s password is a part of the saved hash itself, so no need to keep it separately.

1npm install bcrypt
2npm install --save-dev @types/bcrypt
1const passwordInPlainText = '12345678';
2const hashedPassword = await bcrypt.hash(passwordInPlaintext, 10);
3 
4const doPasswordsMatch = await bcrypt.compare(passwordInPlaintext, hashedPassword);
5console.log(doPasswordsMatch); // true

Generally speaking, an amount of 10 salt rounds should be fine. As you can see the hashing and comparing strings is asynchronous – this is because the hashing done by bcrypt is intensive for the CPU and hashing strings it synchronously would block the application. Our bcrypt implementation uses a thread pool that allows the algorithm to run in an additional thread. Thanks to that, our app is free to do other tasks while waiting for the hash to be generated.

In this example I use async/await. If you would like to know more about it, check out Explaining async/await. Creating dummy promises

Registration and logging in implementation

Knowing all that we can implement the basics of registration and logging in functionalities.

src/authentication/authentication.controller.ts
1import * as bcrypt from 'bcrypt';
2import * as express from 'express';
3import UserWithThatEmailAlreadyExistsException from '../exceptions/UserWithThatEmailAlreadyExistsException';
4import WrongCredentialsException from '../exceptions/WrongCredentialsException';
5import Controller from '../interfaces/controller.interface';
6import validationMiddleware from '../middleware/validation.middleware';
7import CreateUserDto from '../users/user.dto';
8import userModel from './../users/user.model';
9import LogInDto from './logIn.dto';
10 
11class AuthenticationController implements Controller {
12  public path = '/auth';
13  public router = express.Router();
14  private user = userModel;
15 
16  constructor() {
17    this.initializeRoutes();
18  }
19 
20  private initializeRoutes() {
21    this.router.post(`${this.path}/register`, validationMiddleware(CreateUserDto), this.registration);
22    this.router.post(`${this.path}/login`, validationMiddleware(LogInDto), this.loggingIn);
23  }
24 
25  private registration = async (request: express.Request, response: express.Response, next: express.NextFunction) => {
26    const userData: CreateUserDto = request.body;
27    if (
28      await this.user.findOne({ email: userData.email })
29    ) {
30      next(new UserWithThatEmailAlreadyExistsException(userData.email));
31    } else {
32      const hashedPassword = await bcrypt.hash(userData.password, 10);
33      const user = await this.user.create({
34        ...userData,
35        password: hashedPassword,
36      });
37      user.password = undefined;
38      response.send(user);
39    }
40  }
41 
42  private loggingIn = async (request: express.Request, response: express.Response, next: express.NextFunction) => {
43    const logInData: LogInDto = request.body;
44    const user = await this.user.findOne({ email: logInData.email });
45    if (user) {
46      const isPasswordMatching = await bcrypt.compare(logInData.password, user.password);
47      if (isPasswordMatching) {
48        user.password = undefined;
49        response.send(user);
50      } else {
51        next(new WrongCredentialsException());
52      }
53    } else {
54      next(new WrongCredentialsException());
55    }
56  }
57}
58 
59export default AuthenticationController;
In this example the return of  this.user.create and  this.user.findOne is a MongoDB document. The actual data is represented in  user._doc and user.password  is just a getter that returns the data from  user._doc.password. To prevent sending the password back with a response you could also do  delete user._doc.password, but setting the   user.password to undefined also does the trick and there is no trace of the password in the response.

I created a few additional files along the way, such as exceptions and DTO classes used for validation that we covered in the previous part of the tutorial. You can check them out in the repository. In the AuthenticationController above we created two route handlers:   /auth/register and  /auth/login with some basic error handling, such as not allowing more than one person with the same email.

A thing worth noticing is that we don’t make it clear whether it was the username or the password that the user got wrong when attempting to log in. Thanks to displaying a generic error message we prevent potential attackers from getting to know any valid usernames without knowing the passwords.

In the example, we create new users and let them access their data. The crucial thing to implement now is a way for them to authenticate to other parts of our application.

Authentication with JWT tokens

We want to restrict the access to certain parts of our application so that only registered users can use it. In the application that we are using as an example, such a part is creating posts. To implement it we need to create a certain way for users to authenticate and let us know that the request that they send is legitimate. A simple way to do it is with the usage of JSON Web Tokens. JWT is a piece of JSON data that is signed on our server using a secret key when the user is logged in and then sent to him in. When he makes other requests, he sends this token in the headers so that we can encode it back using the same secret key. If the token is valid, we know who the user that made the request is.

Signing tokens

The first thing to implement is creating the tokens. To do this, we use an implementation of JSON Web Tokens available in the NPM.

1npm install jsonwebtoken
2npm install --save-dev @types/jsonwebtoken
1interface TokenData {
2  token: string;
3  expiresIn: number;
4}
1interface DataStoredInToken {
2  _id: string;
3}
1private createToken(user: User): TokenData {
2  const expiresIn = 60 * 60; // an hour
3  const secret = process.env.JWT_SECRET;
4  const dataStoredInToken: DataStoredInToken = {
5    _id: user._id,
6  };
7  return {
8    expiresIn,
9    token: jwt.sign(dataStoredInToken, secret, { expiresIn }),
10  };
11}

To the environment variables covered in the previous part of the tutorial, we added the JWT secret key. It can be any string but remember not to share it with anyone because using it they would be able to encode and decode tokens in your application. To generate a token we also should set its expiry time to increase security – this is because if someone’s token is stolen, the attacker has access to the application similar as if he would have the username and the password. Thanks to setting an expiry time, the issue is a bit smaller because the token expires soon anyway.

In the example above we encode the id of a user in the token so that when he authenticates, we know who he is. You could put more data there such as the name of the user to avoid fetching it from the database, but if the user changes for example his name, the data in the token wouldn’t be up-to-date until a new token is created.

Now we can update the code of our AuthenticationController.

1private registration = async (request: express.Request, response: express.Response, next: express.NextFunction) => {
2  const userData: CreateUserDto = request.body;
3  if (
4    await this.user.findOne({ email: userData.email })
5  ) {
6    next(new UserWithThatEmailAlreadyExistsException(userData.email));
7  } else {
8    const hashedPassword = await bcrypt.hash(userData.password, 10);
9    const user = await this.user.create({
10      ...userData,
11      password: hashedPassword,
12    });
13    user.password = undefined;
14    const tokenData = this.createToken(user);
15    response.setHeader('Set-Cookie', [this.createCookie(tokenData)]);
16    response.send(user);
17  }
18}
19 
20private loggingIn = async (request: express.Request, response: express.Response, next: express.NextFunction) => {
21  const logInData: LogInDto = request.body;
22  const user = await this.user.findOne({ email: logInData.email });
23  if (user) {
24    const isPasswordMatching = await bcrypt.compare(logInData.password, user.password);
25    if (isPasswordMatching) {
26      user.password = undefined;
27      const tokenData = this.createToken(user);
28      response.setHeader('Set-Cookie', [this.createCookie(tokenData)]);
29      response.send(user);
30    } else {
31      next(new WrongCredentialsException());
32    }
33  } else {
34    next(new WrongCredentialsException());
35  }
36}
37 
38private createCookie(tokenData: TokenData) {
39  return `Authorization=${tokenData.token}; HttpOnly; Max-Age=${tokenData.expiresIn}`;
40}

When the user registers or logs in, we create the token and send it to him with the request in the Set-Cookie header.

If you would like to know more about cookies and why should we use the HttpOnly directive, check out Cookies: explaining document.cookie and the Set-Cookie header

Validating the token using middleware

We now expect our users to send the JWT in the form of cookies along with every request that they make. Since the cookie is just a simple string, for our convenience we use the cookie middleware that transforms it into an object.

1npm install cookie-parser
2npm install --save-dev @types/cookie-parser
src/app.ts
1import * as cookieParser from 'cookie-parser';  
2import * as express from 'express';
3 
4class App {
5  public app: express.Application;
6 
7  // (...)
8 
9  private initializeMiddlewares() {
10    this.app.use(bodyParser.json());
11    this.app.use(cookieParser());
12  }

Thanks to  cookie-parser we have the contents of the cookies accessible through  request.cookies.

Now we can create the middleware that checks the JWT token that the user sends. If the operation succeeds, the function appends the user data to the request object.

src/interfaces/requestWithUser.interface.ts
1import { Request } from 'express';
2import User from 'users/user.interface';
3 
4interface RequestWithUser extends Request {
5  user: User;
6}
7 
8export default RequestWitUser;
src/middleware/auth.middleware.ts
1import { NextFunction, Response } from 'express';
2import * as jwt from 'jsonwebtoken';
3import AuthenticationTokenMissingException from '../exceptions/AuthenticationTokenMissingException';
4import WrongAuthenticationTokenException from '../exceptions/WrongAuthenticationTokenException';
5import DataStoredInToken from '../interfaces/dataStoredInToken';
6import RequestWithUser from '../interfaces/requestWithUser.interface';
7import userModel from '../users/user.model';
8 
9async function authMiddleware(request: RequestWithUser, response: Response, next: NextFunction) {
10  const cookies = request.cookies;
11  if (cookies && cookies.Authorization) {
12    const secret = process.env.JWT_SECRET;
13    try {
14      const verificationResponse = jwt.verify(cookies.Authorization, secret) as DataStoredInToken;
15      const id = verificationResponse._id;
16      const user = await userModel.findById(id);
17      if (user) {
18        request.user = user;
19        next();
20      } else {
21        next(new WrongAuthenticationTokenException());
22      }
23    } catch (error) {
24      next(new WrongAuthenticationTokenException());
25    }
26  } else {
27    next(new AuthenticationTokenMissingException());
28  }
29}
30 
31export default authMiddleware;

The function above verifies the JWT token using the same secret string that we used to create it. If the token is wrong, or it expired, the  jwt.verify function throws an error and we need to catch it.

Using the authentication middleware

We can use the middleware above in a few ways. One of them would be to apply it to a whole controller.

1this.router.use(this.path, authMiddleware);

The issue with this is that we want everyone to be able to see our posts, guests included. We can apply the middleware for a specific handler:

1this.router.post(this.path, authMiddleware, validationMiddleware(CreatePostDto), this.createPost);

That would mean adding it to every handler separately. To make our code shorter, we can create a chain of route handlers.

1private initializeRoutes() {
2  this.router.get(this.path, this.getAllPosts);
3  this.router.get(`${this.path}/:id`, this.getPostById);
4  this.router
5    .all(`${this.path}/*`, authMiddleware)
6    .patch(`${this.path}/:id`, validationMiddleware(CreatePostDto, true), this.modifyPost)
7    .delete(`${this.path}/:id`, this.deletePost)
8    .post(this.path, authMiddleware, validationMiddleware(CreatePostDto), this.createPost);
9}

Using the  route.all in such a way applies the middleware only to the route handlers in the chain that match the  `${this.path}/*` route, including  POST /posts.

Now the user data is available in the  createPost function. Let’s use it to save the id of the post author.

1private createPost = async (request: RequestWithUser, response: express.Response) => {
2  const postData: CreatePostDto = request.body;
3  const createdPost = new this.post({
4    ...postData,
5    authorId: request.user._id,
6  });
7  const savedPost = await createdPost.save();
8  response.send(savedPost);
9}

Logging out

The thing with JWT is that it is stateless. It means that you can’t set the token to be invalid on demand. The easiest way is just to implement logging out as removing the token from a browser. Since the cookies storing the token are HttpOnly, we create an endpoint that serves that purpose.

1private initializeRoutes() {
2  this.router.post(`${this.path}/logout`, this.loggingOut);
3}
4 
5private loggingOut = (request: express.Request, response: express.Response) => {
6  response.setHeader('Set-Cookie', ['Authorization=;Max-age=0']);
7  response.send(200);
8}

After requesting this endpoint from the browser, the cookie is removed. The issue with that is the fact that the token that was deleted from the browser is still valid. It will expire after a certain amount of time if you set it up this way, but If you want to you can create a blacklist of tokens in your database and every time someone accesses the application check if his token is blacklisted.

Summary

In this article, we covered registering and logging in users in the Typescript Express application. To implement it we’ve got to know how to hash a password using bcrypt to keep it safe. The authentication that we implement here is done using JSON Web Tokens (JWT) that provide an easy way to identify the users and validate requests. Thanks to all that work we implemented a crucial part of a web application. Stay tuned because there are still things to cover!