NestJS で簡易的なパスワード認証を実装する

この記事は NestJS Advent Calendar 2019 12日目の記事です。昨日は私 @ci7lus の [NestJS Response ヘッダー 変え方](https://qiita.com/ci7lus/items/c50176d4d1a5b8ab021c) でした。
今日は NestJS における簡易的なパスワード認証の実装を紹介します。
NestJS における認可・認証は Guards という機能を用います。

## Guards とは
参考: [Guards | NestJS - A progressive Node.js web framework](https://docs.nestjs.com/guards)

Guards は、コントローラーにおいてアクセス元の情報に合わせた認可を行う仕組みです。
Express などにおいて認可は Middleware の一部として実装されますが、NestJS においては専用のモジュールとして実装されているので、これを利用することでより宣言的で確実な認可を行うことができます。
上記ドキュメントでは、プレーンな Guards の拡張による実装が紹介されていますが、今回行う簡易的なパスワード認証は `@nestjs/passport` を利用します。

## `@nestjs/passport` を利用する
参考: [Authentication | NestJS - A progressive Node.js web framework](https://docs.nestjs.com/techniques/authentication)

`@nestjs/passport` は Passport を NestJS に組み込んで利用するための NestJS 公式追加ライブラリです。
これを用いることで、既存する Passport の Strategy を Guards として取り込むことができます。

## 実装例
今回は、管理画面に対して認証を掛けるというシチュエーションで、認証情報がハードコーディングされた認証を実装します。

サンプルコードはこちら: https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day12-admin-auth

環境としては CLI で初期化したプロジェクトを想定していますので、`nest new nestjs-passport-auth` を実行して、次に進んでください。

### 必要なライブラリの追加
先程挙げた `@nestjs/passport` のほか、今回必要なライブラリをインストールします。

```bash
$ yarn add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
$ yarn add -D @types/passport-local @types/passport-jwt
```

### admin モジュールの作成
次に、admin モジュールとそれに付随するファイルを CLI で作成します。

```bash
$ nest g module admin
$ nest g service admin
$ nest g controller admin
```

これで、`src/admin` の中にモジュール、サービス、コントローラがそれぞれ作成されます。

### 認可の条件を設定する

次に、 `passport-local` を用いたパスワード認証を受け持つ Strategy を実装し、認可の条件を設定します。

```src/admin/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super();
  }

  validate(username: string, password: string): boolean {
    if (username === 'admin' && password == 'password') {
      return true;
    } else {
      throw new UnauthorizedException();
    }
  }
}
```

これでユーザー名が admin、パスワードが password のときに認可を行う Strategy ができました。
利用できるようにモジュールに追加しておきましょう。

```src/admin/admin.module.ts
import { Module } from '@nestjs/common';
import { AdminService } from './admin.service';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { AdminController } from './admin.controller';

@Module({
  imports: [PassportModule],
  providers: [AdminService, LocalStrategy],
  controllers: [AdminController],
})
export class AdminModule {}
```
これで、LocalStrategy が Admin モジュールで利用できるようになりました。

### ログインできるようにする
次に上で作った Strategy を用いてログインができるようにします。
コントローラーに対してログインするエンドポイントを追加します。

```src/admin/admin.controller.ts
import { Controller, UseGuards, Post } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AdminService } from './admin.service';

@Controller('admin')
export class AdminController {
  constructor(private readonly adminService: AdminService) {}

  @UseGuards(AuthGuard('local'))
  @Post('/login')
  login() {
    return true;
  }
}
```

試しにログインしてみましょう。`yarn start:dev` でサーバを立ち上げた後、curl で以下のリクエストを行ってみてください。

```bash
$ curl -X POST http://localhost:3000/admin/login -d '{"username": "admin", "password": "password"}' -H "Content-Type: application/json"
true
```

true と帰ってきましたか？戻り値が true ということは、エンドポイントの上で設定している Strategy がリクエストに対して認可をしてくれたことになります。
試しに、誤った認証情報でリクエストしてみましょう。

```bash
$ curl -X POST http://localhost:3000/admin/login -d '{"username": "admin", "password": "invalid password!"}' -H "Content-Type: application/json"
{"statusCode":401,"error":"Unauthorized"}
```

このように、ちゃんと弾いてくれます。

しかし、管理画面に対して認証をかけたいという要件には、これではまだ不足しています。
なぜなら、ログインリクエスト自体は認可されるようになりましたが、その認可状態が他のリクエストに対して反映されないからです。
次に、認可状態を引き続き利用できるようにしてみましょう。

### アクセストークンを用いる
認可状態を維持するためには、クライアント側が認可済みであるという情報を保持できる必要があります。今回は jwt 形式のアクセストークンを発行し、それをヘッダーに設定してもらうことで認可状態を判断します。
また、NestJS には `@nestjs/jwt` という公式ライブラリがあります。これを用いると、各クラスに対して jwt を簡単に扱えるインスタンスを DI させることができます。
まず、トークンの発行に必要なシークレットキーを設定します。

```src/admin/secrets.ts
export const JWT_SECRET_KEY = 'jwtSecretKey';
```

これを用いる Strategy をもう1つ作ってみましょう。

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

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

  async validate(payload: { isAdmin: boolean }) {
    if (payload.isAdmin === true) {
      return true;
    } else {
      return false;
    }
  }
}
```

利用できるようにモジュールに追加しておきましょう。

```src/admin/admin.module.ts
import { Module } from '@nestjs/common';
import { AdminService } from './admin.service';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { JwtModule } from '@nestjs/jwt';
import { AdminController } from './admin.controller';
import { JwtStrategy } from './jwt.strategy';
import { JWT_SECRET_KEY } from './secrets';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: JWT_SECRET_KEY,
      signOptions: { expiresIn: '600s' },
    }),
  ],
  providers: [AdminService, LocalStrategy, JwtStrategy],
  controllers: [AdminController],
})
export class AdminModule {}
```

この設定では、発行された認可情報が600秒後に失効するようになっています。

次に管理画面を想定したエンドポイントを作成し、アクセストークンを用いて保護してみます。

### アクセストークンの発行と確認

まず、サービスに対してアクセストークンを発行する機能を追加してみましょう。

```src/admin/admin.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AdminService {
  constructor(private readonly jwtService: JwtService) {}
  sign() {
    return { access_token: this.jwtService.sign({ isAdmin: true }) };
  }
}
```

先程、トークンのペイロードにおいて `isAdmin` が true であるかどうかの確認を入れたので、発行側でその値になるようにしました。

次に、ログインを行うエンドポイント、そして発行されたアクセストークンを用いてステータスを確認するエンドポイントを作ってみましょう。

```src/admin/admin.controller.ts
import { Controller, UseGuards, Post, Get } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AdminService } from './admin.service';

@Controller('admin')
export class AdminController {
  constructor(private readonly adminService: AdminService) {}

  @UseGuards(AuthGuard('local'))
  @Post('/login')
  login() {
    return this.adminService.sign();
  }

  @UseGuards(AuthGuard('jwt'))
  @Get('/status')
  status() {
    return true;
  }
}
```

login では true の代わりにアクセストークンを返すようになり、新しく追加した status ではリクエストに含まれたアクセストークンを確認し、正しいものであれば true を返すようになりました。
実際にトークンの発行とステータスの確認を行ってみましょう。

```bash
$ curl -X POST http://localhost:3000/admin/login -d '{"username": "admin", "password": "password"}' -H "Content-Type: application/json"
{"access_token":"発行されたアクセストークン"}
```

アクセストークンが帰ってきました。これを用いて status に対してリクエストを行ってみます。

```bash
$ curl http://localhost:3000/admin/status -H "Authorization: Bearer 発行されたアクセストークン"
true
```

true が帰ってきました。試しに間違ったアクセストークンを用いてアクセスを行ってみましょう。

```bash
$ curl http://localhost:3000/admin/status -H "Authorization: Bearer 間違ったアクセストークン"
{"statusCode":401,"error":"Unauthorized"}
```

ちゃんと弾いてくれました。
これで実装は完了です。

## おわりに
今回はチュートリアルを倣い、管理画面を想定した簡易的なパスワード認証と、認可情報の発行・利用までを実践してみました。
NestJS は公式ライブラリが充実しており、このような実装も比較的簡単に行うことができます。

明日は @potato4d さんの [NestJS API バージョニング](https://qiita.com/potato4d/items/2d5dc727d33e06260a44) です。