Implementing Guards NestJS. Role Based Authentication and Authorization

At the moment, there are not as many publications about the data protection guards tool as it deserves. Basically this is technical documentation from the developer https://docs.nestjs.com/guards . To fill this gap, consider our case of implementing guards to protect data from a user who does not have sufficient rights to receive and / or change them. The description of the guards injection procedure is accompanied by code examples.

To avoid confusion, here are the definitions of the concepts of authentication and authorization, which will be used below.

So, authentication is the process of verifying the identity of a user or device in order to gain authorized access to sensitive information or systems. That is, checking that you are really the person you claim to be.

Authorization is the process of determining whether a user or device has the necessary rights to access a specific resource, in our case, requests (Mutation, Query …).

guard functions

Guards determine whether a request will be processed by the router or not, depending on certain conditions (in our case, roles). This is often referred to as authorization.. Authorization is typically handled by middleware in traditional Express applications. But middleware is inherently limited. It does not know which handler will be executed after the next() function is called. On the other hand, guards have access to the ExecutionContext instance and thus know exactly what will be executed next. They are designed to insert processing logic at exactly the right point in the request/response cycle, and do so declaratively.

What is a token?

This is a JSON object where information about the user is stored in a protected form. The token is assigned to the user who has been authenticated.

Implementing guards

1. Create a token-based authentication service

Since the prerequisite for guards to work is user authentication, let’s look at the token-based authentication process. If the user is authenticated for the first time by entering a login and password, the service creates a token and assigns it to the user.

Creation of a token

To work with the token, the jsonwebtoken library is used. You will also need a secret key object, which is created by the random value generator. Since this object is responsible for the security of the system, it must be protected from getting to third parties. It is recommended to move it to environment variables.

The created token object contains the user id and an array of roles const data = {id: user.id, roles: roles}.

The token is created using the method sign from the jsonwebtoken library.

const token = jwt.sign({ data }, secretKey);

After assigning a token to a user, it is necessary to authenticate the user using a service to get the data object from the token. For this, the method is used verify from the jsonwebtoken library, as well as secretKey.

jwt.verify(token, secretKey);

The next step is to add the data object to the context, which in turn contains the id: user.idroles: roles.

It is advisable to immediately add the User entity to the context, i.e. additionally write a service that will receive the User entity from the database by user.id ('select * from table_user where id = ?‘).

The process of creating a context is not covered here, as this is a separate issue. It is assumed that the application already has a context.

2. Customization Guards

The process of assigning access rights to a particular request is simple. When creating a Mutation, Query or Subscription, we add the @SetMetadata annotation, and the @UseGuards(RolesGuard) annotation must also be assigned to the class.

For example:

import { UseGuards, SetMetadata } from @nestjs/common';

@UseGuards(RolesGuard)

export class ClassName {

  constructor() {}

 @Mutation()

 @SetMetadata('roles', ['USER', 'ADMIN'])

....

  }

}

It follows from this code that users with ‘USER’, ‘ADMIN’ rights have access to Mutation.

So how does the RolesGuard class work.

It’s very simple – the class actually compares two arrays. The first array is a list of the user’s roles taken from the context. The second array is the array that is passed using the SetMetadata annotation. And if at least one role matches in the arrays, then the user gets access to this request. Code example:

import { Injectable, CanActivate, ExecutionContext } from @nestjs/common';

import { Reflector } from @nestjs/core';

import { GqlExecutionContext } from @nestjs/graphql';

@Injectable()

export class RolesGuard implements CanActivate {

  constructor(private reflector: Reflector) {}

  async canActivate(context: ExecutionContext) {

    const userRoles = GqlExecutionContext.create(context).getContext<any>().roles;  // список ролей из контекста

    const roles = this.reflector.get<string[]>('roles', context.getHandler());  // список ролей переданный с помощью аннотации SetMetadata

    const matchRoles = (roles: string[], userRoles: string[]) => {

      let check = false;

      roles.forEach((element) => {

        if (userRoles.includes(element)) {

          check = true;

        }

      });

      return check;

    };

    return matchRoles(roles, userRoles );

  }

}

This completes the description of the introduction of guards, but for completeness of the demonstration of the functionality, one more example of routing within the request should be given:

@Query(() => [Product], { name: 'findAllProduct' })

 @SetMetadata('roles', ['USER', 'ADMIN'])

  findAllProduct@CurrentUser() user?: User) {

    if(user.roles.includes('ADMIN')){

      return this.productService.findAll();

    }

    return this.productService.findAllByUserId(user.id);

  }

This request is available to users with the roles ‘USER’, ‘ADMIN’. Users receive a list of available products in the database. The argument in this request is the user entity, which is stored in the context. This entity contains the following fields id, roles… . Knowing the role of each user, we can perform additional routing. For example, a user with ‘ADMIN’ rights will be given the entire list of products, and a user with ‘USER’ rights will be given only those products that are available in his region. Thus, the user, having made a request, will receive exactly the data that is intended for him.

You can also use guards in ResolveField and thus hide the fields from the user who does not have enough rights to receive them, but here you need to understand that the query execution time will increase, since the check will be called every ResolveField.

Using guards saves a developer a lot of time, allowing you to write less code, which also becomes more understandable. This is an important factor in large projects, so any new developer can easily understand the authorization system. Perhaps this article will draw the attention of developers to this very useful tool with the subsequent use of guards in their projects.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *