1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2022-03-11

概要

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

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

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

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

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を発行するサービスクラスを生成

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

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をフロント側に返す

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 期限が切れている 等があれば
リクエストを弾くことができるようになります

@UseGuards(AuthGuard('jwt'))
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) {}
}
1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?