📌

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

2022/12/04に公開

NestJS における Guard

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

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

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

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

auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

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

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

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

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

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

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

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

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

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

execution-context.interface.d.ts
// ArgumentsHostをextendsしているので、ハンドラーのリクエストオブジェクトなどにもアクセスできる
export interface ExecutionContext extends ArgumentsHost {
    // 現在のハンドラーが属するコントローラーのクラスの型を返す
    getClass<T = any>(): Type<T>;

    // リクエストパイプラインの中で、次に起動されるハンドラーへの参照を返す
    getHandler(): Function;
}

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

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

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

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

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

roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler()); // ここでhandlerのメタデータを取得している
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest(); // ここでハンドラーにきたリクエストを取得している
    const user = request.user;
    return matchRoles(roles, user.roles); // matchRolesの実装はいい感じにやる
  }
}

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

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

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

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

参考

https://docs.nest-book.jp/ndebunestjs/guardtoha
https://docs.nestjs.com/guards

Discussion