DEV Community

kim-jos
kim-jos

Posted on • Edited on

Authentication(JWT) & Authorization

This post will go over authentication and authorization. The difference between them is that authentication deals with whether a user is logged in or not and authorization deals with whether that user is authorized to carry out some action. For example, if some actions of creating or deleting information in the database is allowed only for users that are "admin" status this is considered authorization.

The project I was working on implemented authentication using JSON Web Tokens(JWT) and authorization using guards provided by NestJS.

Authentication

I'm going to briefly go over how JWT works because I tried implementing it using the documentation provided online but without a basic understanding it was hard to customize it according to my needs. JWT is basically a token provided by the server side that the user uses to access information. For example, when a user logs in the log in info(email, password) is verified and the server responds with a token that the user uses in the client side to create, read, update, or delete information.

How does NestJS implement authentication?
NestJS uses PassportJS to implement JWT authentication. I'm not going to go over the installation because it is well documented. If you follow the NestJS's documentation to implement JWT authentication you will probably have to create a bunch of files and I intend to go over how those files interact with each other because I think that was what really confused me in the beginning. The relevant files are: 1) auth.service, 2) jwt-auth.guard, and 3) jwt.strategy

The two big steps in implementing authentication are 1) signing in by receiving a JWT from the server and 2) using guards to prevent access to unauthenticated users.

Step 1: Signing in

To begin, the order in which the files are executed is 1) auth.service, 2) jwt.strategy, then 3) jwt-auth.guard.

  1. API call to localhost:3000/auth/login
@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) { }

  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req); // this is the method that we're going over
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. The "login" method in the service looks like this:
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService
  ) { }

  async validateUser(email: string, pass: string): Promise<any> {
    const user = await this.usersService.findByEmail(email);

    if (user && await bcrypt.compare(pass, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(req: any) { // LOOK HERE!
    console.log('authService', req.body)
    const { email, password } = req.body
    const user = await this.usersService.findByEmail(email)
    const { role, id } = user;

    const validatedUser = await this.validateUser(email, password);
    if (!validatedUser) throw new UnauthorizedException('Unauthorized user');

    const payload = { email, password, role, id };
    const accessToken = this.jwtService.sign(payload) // this part signs the user in!! "sign" is a method provided by JWT
    return { accessToken };
  }
}
Enter fullscreen mode Exit fullscreen mode

By this part we're signed in!!

Step 2: Using guards to prevent access to unauthenticated users

Only people with this JWT token will be able to do CRUD operations in other parts of the app. In this example, only logged in users will be able to create, delete, etc. a "menu".

In NestJS you can use a "guard" to prevent unauthenticated users from accessing routes. If you don't have a JWT in the header of your HTTP request you will be denied access.

@UseGuards(JwtAuthGuard, RolesGuard) //this is the JWT Auth Guard
@Controller('menus')
export class MenusController {
  constructor(private readonly menusService: MenusService) { }

  @Roles(Role.Admin)
  @Post()
  create(@Body() createMenuDto: CreateMenuDto) {
    return this.menusService.create(createMenuDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, this is the hard part. How does this "JwtAuthGuard" work?
Im going to break it down into 3 mains steps.
1) In your auth.service file you sign a payload which gives you an access token. 2) The information in the payload that you signed is sent to the jwt.strategy file where it is validated and you can choose to send back information of your choice(I think that there is a jwt best practice here but I am currently unaware of it). 3) The information you return in the jwt.strategy file then goes to the jwt-auth.guard file where it is returned in the "handleRequest" method as the second argument named "user". If there is no user then the guard throws an error, preventing the user from accessing the route.

Authorization

The project that I was working on had 2 types of users: a normal user and an admin user. The admin user is the only type of user that can do CRUD operations while a normal user can only "get" information.

Now there are 3 main files that work together to implement an authorization guard: 1) roles.guard, 2) roles.decorator, and 3) roles.types. The titles are self-explanatory, there is a roles guard and a decorator and a files to take care of the types of roles there are.

Let's take a look at the menu.controller again.

@UseGuards(JwtAuthGuard, RolesGuard) // RolesGuard goes here!!
@Controller('menus')
export class MenusController {
  constructor(private readonly menusService: MenusService) { }

  @Roles(Role.Admin)  // ONLY admins can access this route
  @Post()
  create(@Body() createMenuDto: CreateMenuDto) {
    return this.menusService.create(createMenuDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

You will be able to see that the "create" method is protected by a "Roles(Role.Admin)" decorator. The "Role.Admin" part is passed into the roles.guard file as a reflector. The biggest problem I faced was that I had trouble getting the user information in the HTTP request. The reason I had to get the user information form the HTTP request was because NestJS guards cannot use dependency injection which means that you cannot use the user service. This is how it connects with JWT. So, I decided to return the role information in the jwt.strategy file. The jwt.strategy file runs before the guard so the user info is inserted into the request. How I figured this out was that there was a user object in the HTTP request in the roles.guard but not in the auth.controller. So, I realized that it was being inserted somewhere and I realized that it was in the jwt.strategy file. The roles.guard looks like the code below.

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) { } //to get the info from custom decorator(@Roles())

  canActivate(context: ExecutionContext) {

    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass()
    ])
    console.log('rolesGuard', requiredRoles)
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    console.log('rolesGuard', user)
    return requiredRoles.some((role) => user.role?.includes(role));
  }
}
Enter fullscreen mode Exit fullscreen mode

I know that I will probably work with authentication in the future and I think this article a good reminder for myself to understand how it all works. I hope it helps anyone else reading.

For anyone interested this is a link to the project I created.

Top comments (0)