LoginSignup
15
8

More than 5 years have passed since last update.

NestJSのPassportModuleについて調べてみた

Posted at

APIを公開するとき、多くは認証したユーザにだけとか、あるロールを持ったユーザにだけ公開したい場合があると思う。NestJSでは認証を実現する手段として、Node.jsでよく使われるPassportを使う仕組みが用意されている。

ただ、ドキュメントのAuthenticationを読んでも、イマイチ何をしたら良いのか分からなかった。

そこで、コードを読み解いて仕組みを理解した上で、サンプルコードを読んでみる。

サンプルコード

HTTP Bearer(トークン)から、認証されているユーザかどうかを調べるサービスHttpStrategyを、Passportのpassport-http-bearerを使って実現している。これはいったいどういう仕組みなのだろう?

http.strategy.ts
import { Strategy } from 'passport-http-bearer';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class HttpStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(token: string) {
    const user = await this.authService.validateUser(token);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

PassportStrategy( Strategy )関数を調べる

HttpStrategyサービスは、PassportStrategy( Strategy )クラスを継承している。PassportStrategy関数のGitHubソースを読んでみると、この関数はnpmで手に入るPassportの各種Strategyクラスを引数にとり、派生クラスMixinStrategyを作っている。
派生クラスMixinStrategyでは、元々のStrategyに対して以下のような拡張をしている。

  1. validate抽象関数を追加
  2. コンストラクタ追加
  3. 親クラス(Strategy)をインスタンス化
    1. 与えられた引数(any[]型)を親クラスの第1引数に与える
    2. コールバック関数を第2引数に与える
  4. passport.use()で自身を登録する
passport.strategy.ts
import * as passport from 'passport';
import { Type } from '../interfaces';

export function PassportStrategy<T extends Type<any> = any>(
  Strategy: T,
  name?: string | undefined
): {
  new (...args): T;
} {
  abstract class MixinStrategy extends Strategy {
    abstract validate(...args: any[]): any;    // [1]
    // [2]
    constructor(...args: any[]) {
      const callback = async (...params: any[]) => {
        const done = params[params.length - 1];
        try {
          done(null, await this.validate(...params));
        } catch (err) {
          done(err, null);
        }
      };
      // [3-1],[3-2]
      super(...args, (...params: any[]) => callback(...params));
      // [4]
      if (name) {
        passport.use(name, this as any);
      } else {
        passport.use(this as any);
      }
    }
  }
  return MixinStrategy;
}

ここまででひとまず、NestJSがPassportにストラテジを登録してくれる仕組みができていることは理解できた。あとはその中身を調べてみる。

コールバック関数

Strategyクラスのコンストラクタの第2引数であるコールバック関数は以下を行っている。なぜこんなことをするかは、コールバック関数の正体を調べるとわかる。

  1. 与えられたパラメータ配列paramsを展開した上でvalidate抽象関数に渡す
  2. 結果をdone関数(paramsの最後の要素)の第2引数に与える
passport.strategy.ts
      const callback = async (...params: any[]) => {
        const done = params[params.length - 1];
        try {
          done(null, await this.validate(...params));
        } catch (err) {
          done(err, null);
        }
      };
      super(...args, (...params: any[]) => callback(...params));

HTTP Bearerのコールバック関数

PassportのStrategyクラスのソース(先ほどのHTTP Bearer)を読むと、以下のような説明が書かれている。

strategy.js
 * Creates an instance of `Strategy`.
 *
 * The HTTP Bearer authentication strategy authenticates requests based on
 * a bearer token contained in the `Authorization` header field, `access_token`
 * body parameter, or `access_token` query parameter.
 *
 * Applications must supply a `verify` callback, for which the function
 * signature is:
 *
 *     function(token, done) { ... }
 *
 * `token` is the bearer token provided as a credential.  The verify callback
 * is responsible for finding the user who posesses the token, and invoking
 * `done` with the following arguments:
 *
 *     done(err, user, info);
 *

コールバック関数は、Strategyのverify callbackであり、最後の引数がdone関数、その手前までがパラメータ(この場合はtokenだけ)となっている。

その動きは以下のようにすることが求められている。

  • 引数(パラメータ)を元にユーザがいるか調べる
    • 有効なユーザがいるならdone関数の第2引数にユーザを入れてコールする
    • 無効なユーザなら、第1引数にエラーを、第2引数にfalseを入れてコールする
    • 第3引数は追加情報なので自由

PassportStrategyを使うには

これまで調べたことをまとめると以下のようになる。

  • PassportStrategyはnpmで手に入るPassportのStrategyをラップし、NestJSのサービスにすることで、自動的に実体が作られ、Passport.useまでできるようにしている。
  • ユーザはvalidate抽象関数 = Strategyのverify callback内の「パラメータを元にユーザがいるか調べる」部分だけ作れば良くなっている。
passport.strategy.ts
      // ここでPassportの仕組みで使うコールバック関数を定義する
      const callback = async (...params: any[]) => {
        // 最後の引数は done( err, user, info )として扱う
        const done = params[params.length - 1];
        try {
          // 利用者が定義するvalidate抽象関数をコールする
          // うまくいけば、結果=ユーザオブジェクトが第2引数に入る
          done(null, await this.validate(...params));
        } catch (err) {
          // 失敗したら、エラーを第1引数に入れて返す
          done(err, null);
        }
      };
      super(...args, (...params: any[]) => callback(...params));

サンプルを読み直す

NestJSのPassportとの連携機能のおかげで、validate抽象関数を定義するだけで、認証が行えるようになっていることが分かる。validate抽象関数では、Strategyがくれるパラメータ(この場合はtoken)を元にユーザを調べ、そのユーザを返すだけで良いことが分かる。

http.strategy.ts
import { Strategy } from 'passport-http-bearer';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class HttpStrategy extends PassportStrategy(Strategy) {
  // トークンに対応するユーザを調べるサービス「AuthService」をDIする
  constructor(private readonly authService: AuthService) {
    super();
  }

  // HTTP Bearerストラテジがtokenを渡してくれる
  async validate(token: string) {
    // ユーザがいるか調べる
    const user = await this.authService.validateUser(token);
    // いなければ「認証できていない」例外を返す
    // コール元のMixinStrategy側でcatchしてくれる
    if (!user) {
      throw new UnauthorizedException();
    }
    // 有効なユーザを返す
    return user;
  }
}

別のStrategyならどうするか?

使いたいStrategyの説明を見る必要があるが、同じ要領だとすると以下のようになると考えられる。

  1. 使いたいStrategyのverify callbackのパラメータを調べる。(HTTP Bearerの場合 token, done関数だった)
  2. 使いたいStrategyクラスを用いたPassportStrategy(Strategy)を継承したクラスを作る。
  3. 前記クラスの中で validate抽象関数を定義する。
  4. validate抽象関数の引数は、validate callbackのdone以外を順番通りに並べたものとする。(HTTP Bearerの場合tokenだけだった)
  5. validate抽象関数の中身を作る。やることは簡単で、引数(tokenなど)を用いて、該当するユーザがいるかを調べ、いたらそのユーザを返す。いなかったら例外を投げる。

以上がNestJSでのPassportでStrategyを使う方法になるが、実際に組み込んで試したわけではないので、今度passport-google-oauth20で検証してみる。

15
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
8