tobb422のブログ

スタートアップエンジニアの奔走

Nest.js における認証機能の基本的な仕組みについて

はじめに

サーバーサイドのFWとして、Nest.js を利用したアプリを作成中です。
その一環として、今回は認証周りの機能を作ってみました!
認証機能を作るにあたっては、Express でも良く利用されている passport が Nest でも採用されており、
ドキュメント に書いてあった内容やNest の Github にあがっていたイシュー等を参考にしつつ、
色々と手元で動かしてみて、なんとなく形になったので、内容をまとめておきます。
※ Nest は使い始めて間もないFWなので、完全な正解は私も良くわかっていません。
ご容赦ください。

認証の流れ & 実装方針

Nest では、クライアントからリクエストを受けた際に、
Controller に到達する直前に Guard と呼ばれる仕組みを通過させることで認証機能を実装することができます。

f:id:tobb422:20190325223753p:plain
認証の流れ

今回は、認証の責務を持つ Guard なので、 AuthGuard としています。
簡単ではありますが、図にあるようにクライアントからのリクエストが
Controller のアクションに到達する直前で、AuthGuard を通過するようになっています。
この AuthGuard を通過することができたリクエストのみが、無事 Controller のアクションへと到達することができるというわけです。

より詳細な説明をすると、 Guard は、canActivate と呼ばれるメソッドを実装している必要があります。
この canActivate メソッドが true を返す場合に、Guard を通過することができます。

そのため、AuthGuard を作成して、配置すれば良いということになるのですが、
今回は、@nestjs/passport にて、既に用意されているものがありますのでそちらを利用します。

自分で実装をする必要があるのは、どういった認証ロジックにするか ということだけです。
実装の詳細は次の項目でまとめます。

ここまでを基本的な流れとします。
まとめると、 以下のような話でした。

  • Controllerの前段に Guard という機能を置く
  • その Guard の中で認証を判断する
  • 認証で利用する Guard は、@nestjs/passport で既に用意されたものがあるので、そちらを利用する
  • 実装する必要があるのは、認証ロジックをどういったものにするかということ

実装

※ ここから先は、Nest にも記載されている Authentication のドキュメント に補足を入れる形で、説明していきます。
※ 説明を簡易にするため、ドキュメントとは説明の順番を前後している箇所があります。

今回の実装では、 Authorization: Bearer ヘッダ を利用した認証機能を想定した作りになっています。

まず、今回の実装で必要な ライブラリをインストールします。
$ npm install --save @nestjs/passport 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) {
  // AuthService はのちほど、作成いたします!
  constructor(private readonly authService: AuthService) {
    super();
  }

  // この validate メソッドが今回の本丸です!
  // HTTPヘッダの Authorization:Bearer の token を引数として受け取ります。
  // 受け取った引数をもとに、なんらかの認証を行います! 
  // ここで返したオブジェクトは、Controller のアクション内で リクエストに含まれるので、例のように、認証したユーザーを返しておくと良いです。
  async validate(token: string) {
    // token をもとに、該当ユーザーを検索する
    const user = await this.authService.validateUser(token);
    if (!user) {
      // ユーザーがいない場合は、エラー
      throw new UnauthorizedException();
    }
    // ユーザーがヒットした場合は、そのユーザーを返却
    return user;
  }
}

@nestjs/passport の AuthGuard は引数として様々なタイプを受け取ることができるのですが、 AuthGuard('bearer') とすることで、今回実装した HttpStrategy の validate を参照してくれます!

こちらは、HttpStrategy 内で利用した AuthService の実装例です。

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  async validateUser(token: string): Promise<any> {
    // Validate if token passed along with HTTP request
    // is associated with any registered account in the database
    // token にヒットするユーザーを検索して、返す処理を担っています。
    return await this.usersService.findOneByToken(token);
  }
}

あとは、使用したい Controller で AuthGuard を指定します。以下は、例です。

import { Controller, Get, Req, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
import { UsersService } from './users.service'
import { User } from './user.entity'

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get('sample')
  @UserGuards(AuthGuard('bearer')) // AuthGuard を使用することを明記する
  async sample(@Req() req): Promise<User> {
    // HttpStrategy の validate で返却した user は、 
    // リクエスト内のオブジェクトとして参照可能です。(ex) req.user
    return req.user
  }
}

すべての Controller で認証を行いたい場合は、Module に明記することでも可能です。

import { Module } from '@nestjs/common'
import { APP_GUARD } from '@nestjs/core'
import { AuthGuard } from '@nestjs/passport'

@Module({
  providers: [
    AppService,
    {
      provide: APP_GUARD,
      useClass: AuthGuard('bearer'),
    },
  ],
})
export class AppModule {}

以上が具体的な実装例です。

まとめ

Nest.js における認証機能の作り方をまとめてみました。
また、副作用的に、認証機能を作ることで、 Guard の理解も深めることができた気がします!
passport をしようすることで、簡単に実装が可能なので、ぜひ、試してみてください。