ぽぴなび

知って感動した技術情報・生活情報や買ってよかったものの雑記です。

【NestJS x Auth0】NestJSで作成したAPIにAuth0の認証/認可を追加する 〜認可(NestJS)編〜

前回の続きです。

  1. NestJSで作成したAPIにAuth0の認証/認可を追加する 〜準備編〜
  2. NestJSで作成したAPIにAuth0の認証/認可を追加する 〜準備編 その2〜
  3. NestJSで作成したAPIにAuth0の認証/認可を追加する 〜認証編〜
  4. NestJSで作成したAPIにAuth0の認証/認可を追加する 〜認可(Auth0)編〜

前回はAuth0側でRBACの設定をしたので、
今回はNestJS側でRBACを実装していきます。

引き続き以下の記事を参考にしています。

auth0.com

1.カスタムデコレーター(@Permissions)の作成

NestJSでRBACを実現するには認証のときと同じようにNestJSのGuard機能を使い、PermissionGuardを作成します。
PermissionGuardでは、APIの実行に必要なPermissionがアクセストークン内のpermissionsに含まれているかどうか確認したいのですが、 APIごとに必要なPermissionは異なるためPermissionGuardを作成する前にカスタムデコレータ(@Permissions)を作成して必要なPermissionをPermissionGuardに渡せるようにします。

@HogeHogeをデコレーターと呼びます。

使用イメージ

@UseGuardsPermissionGuardを指定し、必要なPermissionを@Permissionsで指定します。

  @UseGuards(AuthGuard('jwt'), PermissionsGuard)
  @Post()
  @Permissions('create:items')
  create(@Body('item') item: Item) {
    this.itemsService.create(item);
  }

カスタムデコレーターもリソースなどと同様、Nest CLIを使って作成します。

# --no-spec はテストファイルを生成しないオプションです
npx nest generate decorator permissions --no-spec

デコレーターの作成はなんとこれで終わりです。
作成したデコレーターでやっていることは@Permissions('create:items')で指定した引数('create:items')をメタデータと呼ばれるフィールドに格納しているだけです。
メタデータに格納しておくことでGuardなどからその値を参照することができます。

デコレーターを作成しなくても@SetMetadata()デコレーターで同様のことができますが公式サイトではデコレーターを実装することが推奨されています。
Guards | NestJS - A progressive Node.js framework

2. PermissionGuardの作成

PermissionGuardに必要なPermissionを渡すことができるようになったので、PermissionGuardを作成します。

作成したら、src/permissions/permissions.guard.tsを以下のように修正します。

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

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

  canActivate(context: ExecutionContext): boolean {
    // メタデータに格納したPermissionを取得
    const routePermissions = this.reflector.get<string[]>(
      'permissions',
      context.getHandler(),
    );

    // トークンに含まれるPermissionを取得
    const userPermissions = context.switchToHttp().getRequest()
      .user.permissions;

    if (!routePermissions) {
      return true;
    }

    // 要求したPermissionがトークンに含まれているかチェック
    const hasPermission = () =>
      routePermissions.every((routePermission) =>
        userPermissions.includes(routePermission),
      );

    return hasPermission();
  }
}

これでPermissionGuardの実装は終了です。

3. PermissionGuardの適用

ItemControllerを修正して、各APIPermissionGuardを適用します。
また、忘れずに@Permissionsデコレーターも設定します。

export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @Get()
  findAll() {
    return this.itemsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.itemsService.findOne(id);
  }

  @Post()
  @UseGuards(AuthGuard('jwt'), PermissionsGuard)
  @Permissions('create:items')
  create(@Body() createItemDto: CreateItemDto) {
    return this.itemsService.create(createItemDto);
  }

  @Patch(':id')
  @UseGuards(AuthGuard('jwt'), PermissionsGuard)
  @Permissions('update:items')
  update(@Param('id') id: string, @Body() updateItemDto: UpdateItemDto) {
    return this.itemsService.update(id, updateItemDto);
  }

  @Delete(':id')
  @UseGuards(AuthGuard('jwt'), PermissionsGuard)
  @Permissions('delete:items')
  remove(@Param('id') id: string) {
    return this.itemsService.remove(id);
  }
}

動作確認

すべての設定が終わったので、動作確認してみます。
まずは管理者(admin Role)で実行してすべての操作ができることを確認します。
トークンは前回使ったAuth0サンプルアプリケーションで取得します。

すべてのAPIが実行できたので、利用者(general Role)ユーザーでやってみます。
削除のときのみ、403 Forbiddenとなったので期待通り動いていそうです。

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

ソースコード

github.com

まとめ

NestJSとAuth0を連携させて簡単なAPIに認証/認可を実装しました(長かった。。。)。
認証/認可というと難しいイメージがありましたが、参考記事としてあげているAuth0のブログにほとんど書いてあるので比較的簡単に実装できました(といっても結構時間かかりましたが。。。)。
寄り道して知ったことや気がついたことについては、気が向いたらまとめようと思います。