Nest.jsでGuardからControllerへデータを渡す方法


Nest.jsでは公式ドキュメントにも書いてあるとおり、Guardを作成する際には

  1. CanActivateインターフェースを継承する
  2. canActivateメソッド中でbooleanを返却する

という実装をすることで対象のリクエストの呼び出し可否をコントロールするというのが基本的な使い方です。
しかし、実務で色々なGuardを実装していると、Guardで取得したデータをControllerでも使いたくなるケースに時々出くわします。
GuardとControllerとで同じデータを取得する処理を重複して実装するのは無駄なので、今回はこのGuard内で取得したデータをControllerへと渡す実装をしてみようと思います。

1. Guardのベースとなる抽象化クラス「BaseGuard」を実装する

公式ドキュメントのPassportの実装を参考にします。
認証用モジュールである@nestjs/passportを利用する際には、Guard内のhandleRequestメソッドの中で任意の検証をしつつ、ユーザー情報を返却するような実装をする必要があります。
例えば、公式ドキュメントにあるjwtの認証を行うGuardでは、下記の様な実装をしています。

jwt-auth.guard.ts
import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    // Add your custom authentication logic here
    // for example, call super.logIn(request) to establish a session.
    return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    // You can throw an exception based on either "info" or "err" arguments
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}

このJwtAuthGuardを対象のControllerにセットすると、requestからuserの情報が取得できる様になります。
このuserの値が、先ほどJwtAuthGuardhandleRequestメソッドで返却していたユーザーの情報になります。

import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './auth/jwt-auth.guard';

@Controller()
export class AppController {
  @UseGuards(JwtAuthGuard)
  @Post('auth/login')
  async login(@Request() req) {
    return req.user;
  }
}

これと似たような実装をすべくAuthGuardの実装を参考にしてみます。
AuthGuardでは、canActivateに以下のような実装をしています。
https://github.com/nestjs/passport/blob/master/lib/auth.guard.ts

    async canActivate(context: ExecutionContext): Promise<boolean> {
      const options = {
        ...defaultOptions,
        ...this.options,
        ...await this.getAuthenticateOptions(context)
      };
      const [request, response] = [
        this.getRequest(context),
        this.getResponse(context)
      ];
      const passportFn = createPassportContext(request, response);
      const user = await passportFn(
        type || this.options.defaultStrategy,
        options,
        (err, user, info, status) =>
          this.handleRequest(err, user, info, context, status)
      );
      request[options.property || defaultOptions.property] = user;
      return true;
    }

これはつまり、handleRequestcanActivate内で呼び出し、handleRequestの戻り値をrequestに設定しているだけということです。
この実装を踏まえ、下記のようなベースとなる抽象化クラスを実装します。

import { CanActivate, Injectable, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export abstract class BaseGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const data = await this.handleRequest(context);

    const request = context.switchToHttp().getRequest();
    request['guard'] = data;

    return !!data;
  }

  abstract handleRequest(context: ExecutionContext): any;
}

BaseGuardでは抽象メソッドhandleRequestを用意し、その戻り値をcanActivateメソッド内でguardという名称でrequestに格納しています。

2. Guardからのデータを取得するデコレーターを実装する。

BaseGuardhandleRequestメソッドでrequestに格納された値を取得するデコレーターを作成します。
request['guard']からデータを取得するだけなので、下記の様な実装になります。

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const GuardResponse = createParamDecorator((property: string, context: ExecutionContext) => {
  const request = context.switchToHttp().getRequest();
  const data = request['guard'];

  return property ? data?.[property] : data;
});

3. BaseGuardを継承したGuardを実装する

サンプルとして、Httpヘッダーにdemo-idというデータが存在するかチェックをするGuardを作成します。

import { Injectable, ExecutionContext } from '@nestjs/common';
import { HttpException, HttpStatus } from '@nestjs/common';
import { Demo, DemoDocument } from '../schemas/demo.schema';
import { BaseGuard } from './base.guard';

@Injectable()
export class DemoIdGuard extends BaseGuard {
  constructor(
    @InjectModel(Demo.name)
    private readonly demoModel: Model<DemoDocument>,
  ) {
    super();
  }

  async handleRequest(context: ExecutionContext): Promise<Demo> {
    const { headers } = context.switchToHttp().getRequest();
    const { 'demo-id': demoId } = headers;

    if (!demoId) {
      throw new HttpException('http header must have a "demo-id" property', HttpStatus.BAD_REQUEST);
    }

    return await this.demoModel.findOne({ _id: demoId }).exec();
  }
}

このGuardではヘッダーから取得したdemo-idの値をkeyに、handleRequestDemoというデータを取得してreturnしています。
Controllerではこのreturnした値を取得することができます。

4. 作成したGuardとデコレーターを使用したAPIを作成する

作成したGuardとデコレーターを使用し、Guardより取得した値を返却するだけのGET APIを作成します。

import { Controller, Get } from '@nestjs/common';
import { GuardResponse } from '../decorators';
import { DemoIdGuard } from '../guards';
import { Demo } from '../schemas/demo.schema';

@Controller('demo')
export class DemoController {
  @UseGuards(DemoIdGuard)
  @Get()
  find(@GuardResponse() demo: Demo): Demo {
    return demo;
  }
}

まとめ

今回はGuardで取得した値をControllerで返却するだけのシンプルな実装であったため、GuardからControllerに値を渡すメリットは薄かったですが、Guardで複雑なバリデーションを実施し、その結果取得した値をControllerでも使いたい場合には、2度同じデータを取得する必要がなく、またGuardとContorollerとで役割を分割するNest.jsらしい実装も守ることができるので、とても見通しの良い実装ができると思います。