📌

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

2022/12/04に公開約3,400字

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

ログインするとコメントできます