[NestJS]JwtStrategyでサービスをインジェクション出来なくてハマった時のメモ

以前に、NestJSのバージョン6くらいの時にできてたことが最新のバージョン（ver9系）だとできなくてかなりの時間を取られたので、解決方法を備忘録として残しておきます。


# バージョン

```bash
$ nest --version
9.1.3
```

# 環境構築

## プロジェクト作成

```bash
nest new jwt-test
cd jwt-test
```

## JWT認証関連のパッケージインストール

[リファレンス](https://docs.nestjs.com/security/authentication#jwt-functionality)に従ってJWTに必要なパッケージをインストール

```bash
npm install --save @nestjs/passport passport
npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
```

※passport-localは今回使わないので、インストールしません


## JwtStrategyの作成

まずはコンストラクタに何も指定しない状態で試します

```src/auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'secret',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

```

## JwtStrategyを使う

```src/auth/auth.controller.ts
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';

@Controller({ path: 'auth' })
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('login')
  login(@Body() body: { username: string; password: string }) {
    return this.authService.login(body);
  }

  /**
   * 認証が必要な処理
   * @returns
   */
  @UseGuards(JwtAuthGuard)
  @Get('test')
  test() {
    return '認証成功';
  }
}
```

# 動作確認してみる

Talend API Testerで動確してみました

- `/auth/test`をトークンなしで呼び出すと401

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/6ae6ce88-c114-f890-953a-59ed7c3902cd.png)

- `/auth/login`でJWTを生成
- `/auth/test`に`Authorization: Bearer {JWT}`をヘッダに付与してもう一度呼び出します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/94d218d5-c863-a71a-0129-c3c2b0a0f695.png)


# 本題：JwtStrategyで追加のチェックをしたい

JWTの認証が通った時に、ユーザーの持つ権限をDBから取得しようとしたんですが、ここでハマりました。。。

以前は以下のように直接インジェクション出来てたんですが、最新版ではできなくなっていました。

```src/auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthService } from 'src/auth/auth.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
    // 直接インジェクション
  constructor(private authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'secret',
    });
  }

  async validate(payload: any) {
    const userId = payload.sub;

    // AuthServiceのメソッド呼び出し
    const user = await authService.validate(userId);
    return {
      userId: user.id,
      username: user.username,
      authorities: user.authorities,
    };
  }
}
```

これで`Unknown authentication strategy "jwt"`エラーとなりました。。。

# 解決法：moduleRefを使う

[リファレンス](https://docs.nestjs.com/security/authentication#request-scoped-strategies)にちっちゃく書いてありました


```src/auth/auth.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { Request } from 'express';
import { AuthService } from './auth.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  // ModuleRefをインジェクション
  constructor(private moduleRef: ModuleRef) {
    super({
      // こいつを指定すると、validateの第一引数にRequestが渡されるようになる
      passReqToCallback: true,
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'secret',
    });
  }

  async validate(request: Request, payload: any) {
    // サービスを取り出す(JwtStrategyでprovideしているサービスしか指定できない？)
    const contextId = ContextIdFactory.getByRequest(request);
    const authService = await this.moduleRef.resolve(AuthService, contextId);

    const userId = payload.sub;

    // UsersServiceのメソッド呼び出し
    const user = await authService.validate(userId);
    return {
      userId: user.id,
      username: user.username,
      authorities: user.authorities,
    };
  }
}

```

説明を読む感じ、RequestScopeを使ってるとコンストラクタで直接インジェクションが出来ないってことですかね？


めちゃくちゃハマりましたが、無事に解決できてよかった。。。