Nest.js Tutorial

Authorization with roles and claims

Marcin Wanago
JavaScriptNestJSTypeScript

So far, in this series, we’ve implemented authentication. By doing that, we can confirm that the users are who they claim to be. In this series, we explain how to implement authentication with JWT tokens or with server-side sessions. We also add two-factor authentication.

While authorization might at first glance seem similar to authentication, it serves a different purpose. With authorization, we check the user’s permission to access a specific resource. A good example would be to allow a user to create posts but not delete them. This article presents two different approaches to authorization and presents how to implement them with NestJS.

While authorization is a separate process, it makes sense to have the authentication mechanism implemented first.

Role-based access control (RBAC)

With role-based access control (RBAC), we assign roles to users. Let’s create an enum containing fundamental roles:

role.enum.ts
1enum Role {
2  User = 'User',
3  Admin = 'Admin',
4}
5 
6export default Role;

We also need a column to define the role of a particular user. Since in this series we use PostgreSQL, we can use the enum type:

1CREATE TYPE user_role AS ENUM ('User', 'Admin');
2 
3CREATE TABLE users (
4  id serial PRIMARY KEY,
5  email text UNIQUE,
6  role user_role
7)

Fortunately, TypeORM supports enums. Let’s add it to the definition of the User entity. We can make it an array to support the user having multiple roles.

user.entity.ts
1import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2import Role from './role.enum';
3 
4@Entity()
5class User {
6  @PrimaryGeneratedColumn()
7  public id: number;
8 
9  @Column({ unique: true })
10  public email: string;
11 
12  @Column({
13    type: 'enum',
14    enum: Role,
15    array: true,
16    default: [Role.User]
17  })
18  public roles: Role[]
19  
20  // ...
21}
22 
23export default User;

Assigning roles to routes

The official NestJS documentation suggests using two separate decorators: one to assign a role to the route and the second one to check if the user has the role. We can simplify that by creating a guard that accepts a parameter. To do that, we need to create a mixin.

role.guard.ts
1import Role from './role.enum';
2import { CanActivate, ExecutionContext, mixin, Type } from '@nestjs/common';
3import RequestWithUser from '../authentication/requestWithUser.interface';
4 
5const RoleGuard = (role: Role): Type<CanActivate> => {
6  class RoleGuardMixin implements CanActivate {
7    canActivate(context: ExecutionContext) {
8      const request = context.switchToHttp().getRequest<RequestWithUser>();
9      const user = request.user;
10 
11      return user?.roles.includes(role);
12    }
13  }
14 
15  return mixin(RoleGuardMixin);
16}
17 
18export default RoleGuard;

An important thing above is that we use the RequestWithUser interface. For the request to contain the user property, we also need to use the JwtAuthenticationGuard:

We implement JwtAuthenticationGuard in API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
posts.controller.ts
1import {
2  ClassSerializerInterceptor,
3  Controller,
4  Delete,
5  Param, ParseIntPipe,
6  UseGuards,
7  UseInterceptors,
8} from '@nestjs/common';
9import PostsService from './posts.service';
10import RoleGuard from '../users/role.guard';
11import Role from '../users/role.enum';
12import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
13 
14@Controller('posts')
15@UseInterceptors(ClassSerializerInterceptor)
16export default class PostsController {
17  constructor(
18    private readonly postsService: PostsService
19  ) {}
20 
21  @Delete(':id')
22  @UseGuards(RoleGuard(Role.Admin))
23  @UseGuards(JwtAuthenticationGuard)
24  async deletePost(@Param('id', ParseIntPipe) id: number) {
25    return this.postsService.deletePost(id);
26  }
27 
28  // ...
29}

If the user does not have the Admin role, NestJS throws 403 Forbidden:

Extending the JwtAuthenticationGuard

The crucial thing about the above code is the correct order of guards. Since decorators run from bottom to top, we need to use the JwtAuthenticationGuard below the RoleGuard.

To deal with the above issue more elegantly, we can extend our JwtAuthenticationGuard:

role.guard.ts
1import Role from './role.enum';
2import { CanActivate, ExecutionContext, mixin, Type } from '@nestjs/common';
3import RequestWithUser from '../authentication/requestWithUser.interface';
4import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
5 
6const RoleGuard = (role: Role): Type<CanActivate> => {
7  class RoleGuardMixin extends JwtAuthenticationGuard {
8    async canActivate(context: ExecutionContext) {
9      await super.canActivate(context);
10 
11      const request = context.switchToHttp().getRequest<RequestWithUser>();
12      const user = request.user;
13 
14      return user?.roles.includes(role);
15    }
16  }
17 
18  return mixin(RoleGuardMixin);
19}
20 
21export default RoleGuard;

Because we call await super.canActivate(context), we no longer need to use both JwtAuthenticationGuard and RoleGuard:

posts.controller.ts
1@Delete(':id')
2@UseGuards(RoleGuard(Role.Admin))
3async deletePost(@Param('id', ParseIntPipe) id: number) {
4  return this.postsService.deletePost(id);
5}

Claims-based authorization

When implementing claims-based authorization, we take a slightly different approach. Instead of defining a few roles, we define multiple permissions.

permission.enum.ts
1enum Permission {
2  DeletePost = 'DeletePost',
3  CreateCategory = 'CreateCategory'
4}
5 
6export default Permission;

Unfortunately, storing all permissions in a single enum might not be a scalable approach. Because of that, we can create multiple enums and merge them.

If you want to read more about merging enums, check this answer on StackOverflow.
categoriesPermission.enum.ts
1enum CategoriesPermission {
2  CreateCategory = 'CreateCategory'
3}
4 
5export default CategoriesPermission;
postsPermission.enum.ts
1enum PostsPermission {
2  DeletePost = 'DeletePost'
3}
4 
5export default PostsPermission;
permission.type.ts
1import PostsPermission from '../posts/postsPermission.enum';
2import CategoriesPermission from '../categories/categoriesPermission.enum';
3 
4const Permission = {
5  ...PostsPermission,
6  ...CategoriesPermission
7}
8 
9type Permission = PostsPermission | CategoriesPermission;
10 
11export default Permission;
Thanks to the above approach, we can use Permission both as a type and as a value.

Let’s use the above type in the user’s entity definition:

user.entity.ts
1import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2import Permission from './permission.type';
3 
4@Entity()
5class User {
6  @PrimaryGeneratedColumn()
7  public id: number;
8 
9  @Column({ unique: true })
10  public email: string;
11  
12  @Column({
13    type: 'enum',
14    enum: Permission,
15    array: true,
16    default: []
17  })
18  public permissions: Permission[]
19  
20  // ...
21}
22 
23export default User;

Doing the above with TypeORM creates an enum that consists of all of the permissions we’ve defined.

We can use the Permission type to create the PermissionGuard:

permission.guard.ts
1import { CanActivate, ExecutionContext, mixin, Type } from '@nestjs/common';
2import RequestWithUser from '../authentication/requestWithUser.interface';
3import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
4import Permission from './permission.type';
5 
6const PermissionGuard = (permission: Permission): Type<CanActivate> => {
7  class PermissionGuardMixin extends JwtAuthenticationGuard {
8    async canActivate(context: ExecutionContext) {
9      await super.canActivate(context);
10 
11      const request = context.switchToHttp().getRequest<RequestWithUser>();
12      const user = request.user;
13 
14      return user?.permissions.includes(permission);
15    }
16  }
17 
18  return mixin(PermissionGuardMixin);
19}
20 
21export default PermissionGuard;
Above, we extend the JwtAuthenticationGuard to avoid having to use two guards in the same way we’ve done with the RoleGuard.

The last step is to use the PermissionGuard on a route:

1@Delete(':id')
2@UseGuards(PermissionGuard(PostsPermission.DeletePost))
3async deletePost(@Param('id', ParseIntPipe) id: number) {
4  return this.postsService.deletePost(id);
5}

Summary

In this article, we’ve implemented both role-based and claims-based authorization. We’ve done that by defining guards using the mixin pattern. We’ve also learned about the enum type built into PostgreSQL. While learning about authorization, we’ve used two different approaches. While both role-based and claims-based authorization would work, the latter is more customizable. As our application grows, we might find that it is easier to use claims because they are more generic.