Nest. js Authorization Verification Method Sample

  • 2021-10-27 06:30:34
  • OfStack

Directory 0x0 Preface
Implementation of 0x1 RBAC
0x2 Claim-based Authorization
0x3 integrated CASL
0x4 PoliceiesGuard

0x0 Preface

System authorization refers to the operation process executed by login users, such as administrator can operate users and website posts on the system, and non-administrator can authorize reading posts and other operations, so the authorization of the system needs authentication mechanism, so the most basic role-based access control system is realized below.

Implementation of 0x1 RBAC

Role-based access control (RBAC) is an access control mechanism independent of the privileges of roles and defined policies. First, create an enumeration information role. enum. ts representing system roles:


export enum Role {
 User = 'user',
 Admin = 'admin'
}

If it is a more complex system, it is recommended that the role information be stored in the database for better management.

Then create the decorator and use @ Roles () to run the resource roles required for specified access, creating roles. decorator. ts:


import { SetMetadata } from '@nestjs/common'
import { Role } from './role.enum'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

The above creates a decorator named @ Roles (), which can be used to decorate any one routing controller, such as user creation:


@Post()
@Roles(Role.Admin)
create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> {
 return this.userService.create(createUserDto)
}

Finally, an RolesGuard class is created, which compares the roles assigned to the current user with the roles required by the current routing controller. In order to access the routing roles (custom metadata), a new roles. guard. ts will be created using the Reflector tool class:


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

import { Role } from './role.enum'
import { ROLES_KEY } from './roles.decorator'

@Injectable()
export class RolesGuard implements CanActivate {
 constructor(private reflector: Reflector) {}

 canActivate(context: ExecutionContext): boolean {
 const requireRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [context.getHandler(), context.getClass()])
 if (!requireRoles) {
  return true
 }
 const { user } = context.switchToHttp().getRequest()
 return requireRoles.some(role => user.roles?.includes(role))
 }
}

Assume that request. user contains the roles attribute:


class User {
 // ...other properties
 roles: Role[]
}

RolesGuard then registers globally with the controller:


providers: [
 {
 provide: APP_GUARD,
 useClass: RolesGuard
 }
]

When a user accesses a request beyond the scope of the role:


{
 "statusCode": 403,
 "message": "Forbidden resource",
 "error": "Forbidden"
}

0x2 Claim-based Authorization

After the identity is created, The system can assign one or more claim permissions to an identity, It means telling the current user what to do, not what the current user is. To realize declaration-based authorization in Nest system, the steps are similar to those of RBAC above, but there is a difference that it is necessary to compare permissions instead of judging specific roles. Each user is assigned a set of permissions, such as setting a @ RequirePermissions () decorator, and then accessing the required permission attributes:


@Post()
@RequirePermissions(Permission.CREATE_USER)
create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> {
 return this.userService.create(createUserDto)
}

Permission represents an enumeration similar to Role in PRAC and contains the permission groups within which the system can access:


export enum Role {
 CREATE_USER = ['add', 'read', 'update', 'delete'],
 READ_USER = ['read']
}

0x3 integrated CASL

CASL is a homogeneous authorization library that can restrict client access to routing controller resources. Installation depends on:


yarn add @casl/ability

The following uses the simplest example to implement the mechanism of CASL, creating two entity classes, User and Article:


import { SetMetadata } from '@nestjs/common'
import { Role } from './role.enum'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

0

The User entity class has two attributes, which are the user number and whether it has administrator privileges.


class Article {
 id: number
 isPublished: boolean
 authorId: string
}

The Article entity class has three attributes, which are the article number and article status (whether published or not) and the author number who wrote the article.

Compose the simplest function according to the above two simplest examples:

Users with administrator privileges can manage all entities (create, read, update, and delete) Users have read-only access to all content Users can update their own articles authorId = = userId Published article cannot delete article. isPublished = = true

For the above functions, you can create an Action enumeration to represent the user's operation on the entity:


export enum Action {
 Manage = 'manage',
 Create = 'create',
 Read = 'read',
 Update = 'update',
 Delete = 'delete',
}

manage is a special keyword in CASL, meaning that anything can be done.

To realize the function, it is necessary to encapsulate the CASL library twice, and execute nest-cli to create the required services:


import { SetMetadata } from '@nestjs/common'
import { Role } from './role.enum'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

3

Define the createForUser () method of CaslAbilityFactory to create objects without users:


import { SetMetadata } from '@nestjs/common'
import { Role } from './role.enum'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

4

Then introduced in CaslModule:


import { SetMetadata } from '@nestjs/common'
import { Role } from './role.enum'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

5

Then introduce CaslModule in any business and then inject it into the constructor to be used:


import { SetMetadata } from '@nestjs/common'
import { Role } from './role.enum'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

6

If the current user is a non-administrator user with normal privileges, you can read articles, but you cannot create new articles and delete existing articles:


const user = new User()
user.isAdmin = false

const ability = this.caslAbilityFactory.createForUser(user)
ability.can(Action.Read, Article) // true
ability.can(Action.Delete, Article) // false
ability.can(Action.Create, Article) // false

This is obviously problematic, and if the current user is the author of the article, he should be able to operate on it:


const user = new User()
user.id = 1

const article = new Article()
article.authorId = user.id

const ability = this.caslAbilityFactory.createForUser(user)
ability.can(Action.Update, article) // true

article.authorId = 2
ability.can(Action.Update, article) // false

0x4 PoliceiesGuard

The simple implementation mentioned above still does not meet the more complex requirements in complex systems, so cooperate with the authentication article in the previous article to extend the class-level authorization policy mode and extend it in the original CaslAbilityFactory class:


import { SetMetadata } from '@nestjs/common'
import { Role } from './role.enum'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

9

Provides support objects and functions for policy checking for each routing controller: IPolicyHandler and PolicyHandlerCallback.

Then create a @ CheckPolicies () decorator to run the policy of specifying access to a specific resource:


export const CHECK_POLICIES_KEY = 'check_policy'
export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers)

Create an PoliciesGuard class to extract and execute all policies that bind the routing controller:


@Injectable()
export class PoliciesGuard implements CanActivate {
 constructor(
 private reflector: Reflector,
 private caslAbilityFactory: CaslAbilityFactory,
 ) {}

 async canActivate(context: ExecutionContext): Promise<boolean> {
 const policyHandlers =
  this.reflector.get<PolicyHandler[]>(
  CHECK_POLICIES_KEY,
  context.getHandler()
  ) || []

 const { user } = context.switchToHttp().getRequest()
 const ability = this.caslAbilityFactory.createForUser(user)

 return policyHandlers.every((handler) =>
  this.execPolicyHandler(handler, ability)
 )
 }

 private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
 if (typeof handler === 'function') {
  return handler(ability)
 }
 return handler.handle(ability)
 }
}

Assuming that request. user contains user instances, policyHandlers is assigned through the decorator @ CheckPolicies (), aslAbilityFactory # create is used to construct an Ability object method to verify that the user has sufficient privileges to perform a specific operation, and then this object is passed to a policy handling method that can implement an instance of a function or class IPolicyHandler, and exposes that the handle () method returns a Boolean value.


@Get()
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))
findAll() {
 return this.articlesService.findAll()
}

You can also define IPolicyHandler interface classes:


export class ReadArticlePolicyHandler implements IPolicyHandler {
 handle(ability: AppAbility) {
 return ability.can(Action.Read, Article)
 }
}

Use the following:


@Get()
@UseGuards(PoliciesGuard)
@CheckPolicies(new ReadArticlePolicyHandler())
findAll() {
 return this.articlesService.findAll()
}

Related articles: