【NestJS】JWTでAPIエンドポイントを保護する

# 概要

`JWT(JSONWebToken)`を利用した`passport-jwt戦略`を実装した際の知見をまとめます

> JWTとは、JSON形式で表現された認証情報などをURL文字列などとして安全に送受信できるよう、符号化やデジタル署名の仕組みを規定した標準規格。IETFによってRFC 7519として標準化されている。

https://docs.nestjs.com/security/authentication#implementing-passport-jwt

https://github.com/mikenicholson/passport-jwt

# `JWT`の認証処理クラスを作成


> JSONWebトークンでRESTfulエンドポイントを保護するためのpassport-jwt戦略を提供します

```ts:src/auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { SCRIPT_ALERT_LIST } from 'src/utilities/masters.utility'
import { AuthService } from '../../../services/auth.service'

/**
 * @description JWTの認証処理を行うクラス
 */
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    // 環境変数を読み込むために利用
    private readonly configService: ConfigService,

    // 認証ユーザーを返却する自作サービスクラス
    private readonly authService: AuthService
  ) {
    super({
      // HTTPヘッダの Authorization bearerからトークンを読み込む
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),

      // トークンの有効期限を検証する 
      ignoreExpiration: false,
      
      // 秘密鍵
      secretOrKey: configService.get('auth.jwt_secret_key'),
    })
  }

  /**
   * JWTバリデーション
   * @param req
   * @param res
   * @param next
   * @returns
   */
  async validate(req: Request) {
    // APIに対してJWTトークンを含んだペイロード情報がリクエストされるので取得
    const payload = req['payload']
    const token = payload && payload.token ? payload.token : null
    
    // 元の秘密鍵とリクエストされた秘密鍵が一致しない場合は認証を弾く
    if (token && token === this.configService.get('auth.jwt_secret_key')) {
      return await this.authService.findMe(payload.account_id)
    } else {
      throw new UnauthorizedException(SCRIPT_ALERT_LIST.AUTH_FAILURE)
    }
  }
}
```

# `JWT`を発行するサービスクラスを生成

https://www.npmjs.com/package/jsonwebtoken

発行した`JWT`の中身を以下のサイトに貼り付けると内容を確認できます

https://jwt.io/

```ts:src/services/auth.service.ts
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { InjectRepository } from '@nestjs/typeorm'
import * as bcrypt from 'bcrypt'
import * as jwt from 'jsonwebtoken'
import dayjs from 'src/commons/customs/dayjs'
import { Repository } from 'typeorm'
import {
  AuthInterFace,
  JwtPayload,
} from '../domain/interfaces/api-response/auth.interface'

@Injectable()
export class AuthService {
  constructor() {
  }

  /**
   * @description Jwtトークン発行
   * @param {Account} account
   * @param {string} path
   * @return {*}  {string}
   * @memberof AuthService
   */
  public createToken(account: AuthInterFace, path: string): string {
    /**
     * 有効期限を設定する 
     * string.endsWith でエンドポイントのURLにより期限を変更している
     */
    const exp = path.endsWith(this.configService.get('auth.samplePath'))
      ? dayjs().add(1, 'year').toDate()
      : dayjs().add(1, 'day').toDate()

    // トークンに組み込むペイロード情報
    const payload: JwtPayload = {
      account_id: account.id,
      login_id: account.login_id,
      token: this.configService.get('auth.jwt_secret_key'),
    }

    // JWT トークンを発行します
    return jwt.sign(
      { payload, exp: exp.valueOf() / 1000 },
      this.configService.get('auth.jwt_secret_key')
    )
  }
}
```

# ログイン時に`JWT`をフロント側に返す

```ts:src/controllers/auth.controller.ts
@Controller('auth')
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private readonly terminalsService: TerminalService
  ) {}

/**
   * @description ログイン-コンソール
   * @param {LoginUserDto} loginUserDto
   * @param {Request} req
   * @return {*}  {Promise<loginDataRO>}
   * @memberof AuthController
   */
  @Post('/login')
  @UseGuards(AuthGuard('local'))
   async login(
    @Body() loginUserDto: LoginUserDto,
    @Req() req: Request
  ): Promise<loginDataRO> {
    const _account = await this.authService.findOne(loginUserDto)
    return returnAccount(_account, req, this.authService)
  }

/**
 * @description ログインしたアカウントの情報を返却
 * @param {Account} AuthInterFace
 * @param {Request} req
 * @param {AuthService} authService
 * @return {*}  {loginDataRO}
 */
const returnAccount = (
  loginAccount: AuthInterFace,
  req: Request,
  authService: AuthService
): loginDataRO => {
  if (!loginAccount) {
    throw new UnauthorizedException()
  }
  const token = authService.createToken(loginAccount, req.url)
  const { id, login_id, name } = loginAccount

  const account = {
    account_id: id,
    name: name,
    login_id: login_id,
    token,
  }
  return { account }
}
```

ここまでで、`APIエンドポイント`にログインリクエストを投げた後に`JWT`を返却する処理が実装できたので、最後に`JWT`の認証処理クラスをセットします

セットしたAPIエンドポイントにリクエストを投げる際に`JWT`が無い or 期限が切れている　等があれば
リクエストを弾くことができるようになります


```ts
@UseGuards(AuthGuard('jwt'))
```


```ts:ContractorController.ts
import {
  Controller,
  UseGuards,
} from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
import { Request } from 'express'

@UseGuards(AuthGuard('jwt'))
@Controller('/contractors')
export class ContractorController {
  constructor(private readonly contractorService: ContractorService) {}
}
```