WebLog

NestJSでGuardを使って認可を実装する

2022/12/04 12:36

NestJS における Guard

Guard とは、@Injectable()デコレータでアノテーションされたクラスで、CanActivate インターフェイスを実装しています。 Guard は一つの責任を持ちます。それらは、実行時に存在する特定の条件に応じて、与えられたリクエストがルートハンドラによって処理されるかどうかを決定します。これは認可(authorization)と言います。

Nest.js におけるGuardは実際には次の条件を全て満たすようなものです:

  • クラスである
  • @Injectable()デコレーターでアノテーションされている
  • CanActivateというインターフェースをimplementsしている
  • canActivateという、ExecutionContext型を引数にとり、同期または非同期で boolean 値(true または false)を返すメソッドを実装している

つまり、以下のような内容になります:

auth.guard.ts
1import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2import { Observable } from 'rxjs';
3
4@Injectable()
5export class AuthGuard implements CanActivate {
6 canActivate(
7 context: ExecutionContext,
8 ): boolean | Promise<boolean> | Observable<boolean> {
9 const request = context.switchToHttp().getRequest();
10 return validateRequest(request);
11 }
12}

このようなテンプレートは以下のnest cliコマンドによっても作成できます:

1# nest g gu auth などでも可能。詳しくは nest --help を参照のこと。
2nest genarate guard auth

そして次のように使います:

cats.controller.ts
1@Controller('cats')
2@UseGuards(RolesGuard) // CatsControllerのハンドラー全体をGuard
3export class CatsController {
4 // いろいろなハンドラーたち
5}

特定のハンドラーだけを Guard することもできます:

cats.controller.ts
1@Controller('cats')
2export class CatsController {
3 @UseGuards(RolesGuard) // 直下のハンドラーだけをguard
4 @Get()
5 async getCats() {
6 return "cats を getするよ"
7 }
8}

canActivate の引数である context: ExecutionContext について

Guard のすごいところはcanActivateの引数のcontextから、「guard がどこにバインドされているか」を知ることができることです

もっと詳しく見ましょう。ExecutionContext 型は以下のような実装になっています:

execution-context.interface.d.ts
1// ArgumentsHostをextendsしているので、ハンドラーのリクエストオブジェクトなどにもアクセスできる
2export interface ExecutionContext extends ArgumentsHost {
3 // 現在のハンドラーが属するコントローラーのクラスの型を返す
4 getClass<T = any>(): Type<T>;
5
6 // リクエストパイプラインの中で、次に起動されるハンドラーへの参照を返す
7 getHandler(): Function;
8}

SetMetadata で特定のハンドラーに情報を付与する

コントローラーの中で、このメソッドは管理者だけにしか実行させたくない、みたいな状況があると思います。 その場合は、Guardに加えてSetMetadataを組み合わせることでいい感じに実装できます。

例えば、CatsController の中で、create メソッドは管理者権限があるユーザーにしか実行できないようにしたいとします。そのようなときは、以下のようにします:

1@Controller("cats")
2@UseGuards(RolesGuard) // CatsControllerのハンドラー全体をGuard
3export class CatsController {
4 @Post()
5 @SetMetadata("roles", ["admin"]) // ハンドラーに対して情報を付与する
6 async create(@Body() createCatDto: CreateCatDto) {
7 this.catsService.create(createCatDto);
8 }
9}

そして、RolesGuardにおいて、ユーザーの role と、ハンドラーの role を検証し、実行可能かどうかを確認します。

roles.guard.ts
1import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2import { Reflector } from '@nestjs/core';
3
4@Injectable()
5export class RolesGuard implements CanActivate {
6 constructor(private reflector: Reflector) {}
7
8 canActivate(context: ExecutionContext): boolean {
9 const roles = this.reflector.get<string[]>('roles', context.getHandler()); // ここでhandlerのメタデータを取得している
10 if (!roles) {
11 return true;
12 }
13 const request = context.switchToHttp().getRequest(); // ここでハンドラーにきたリクエストを取得している
14 const user = request.user;
15 return matchRoles(roles, user.roles); // matchRolesの実装はいい感じにやる
16 }
17}

重要なのは以下の点です:

  • context.getHandler()でハンドラーの情報を取得している
  • reflector というものを使ってハンドラーのメタデータを取得している
  • context.switchToHttp().getRequest()でハンドラーに入ってくるリクエストオブジェクトを取得している

なお、十分な権限を持たないユーザーがエンドポイントにリクエストした場合、NestJS は自動的に以下のレスポンスを返してくれます:

1{
2 "statusCode": 403,
3 "message": "Forbidden resource",
4 "error": "Forbidden"
5}

参考

Error

Not Found

最新の投稿