2023年3月31日

NestJSとPrismaでREST APIを構築する:認証

10分読書

NestJS、Prisma、PostgreSQLを使ってREST APIを構築する本シリーズの第5回チュートリアルへようこそ!このチュートリアルでは、NestJS REST APIにJWT認証を実装する方法を学びます。

Building a REST API with NestJS and Prisma: Authentication

目次

はじめに

本シリーズの前章では、NestJS REST APIでリレーショナルデータを扱う方法を学びました。`User`モデルを作成し、`User`と`Article`モデル間に一対多の関係を追加しました。また、`User`モデルのCRUDエンドポイントも実装しました。

この章では、Passportというパッケージを使用してAPIに認証を追加する方法を学びます。

  1. まず、Passportというライブラリを使用してJSON Web Token (JWT) ベースの認証を実装します。
  2. 次に、bcryptライブラリを使用してパスワードをハッシュ化し、データベースに保存することで保護します。

このチュートリアルでは、前章で構築したAPIを使用します。

開発環境

このチュートリアルを進めるには、以下が必要です。

  • ... Node.jsがインストールされていること。
  • ... DockerDocker Composeがインストールされていること。Linuxをご利用の場合は、Dockerのバージョンが20.10.0以降であることを確認してください。ターミナルでdocker versionを実行することでDockerのバージョンを確認できます。
  • ... *オプションとして*、Prisma VS Code Extensionがインストールされていること。Prisma VS Code拡張機能は、Prismaに非常に優れたIntelliSenseとシンタックスハイライトを追加します。
  • ... *オプションとして*、このシリーズで提供されるコマンドを実行するために、Unixシェル(LinuxやmacOSのターミナル/シェルなど)にアクセスできること。

Unixシェルをお持ちでない場合(例えば、Windowsマシンをご利用の場合)でも、このチュートリアルを進めることは可能ですが、シェルコマンドをマシンに合わせて変更する必要があるかもしれません。

リポジトリのクローン

このチュートリアルの出発点は、本シリーズの第2章の終了時点です。NestJSで構築された基本的なREST APIが含まれています。

このチュートリアルの出発点は、GitHubリポジトリend-validationブランチで利用可能です。開始するには、リポジトリをクローンし、end-validationブランチをチェックアウトしてください。

さて、始めるために以下の操作を行ってください

  1. クローンしたディレクトリに移動する
  1. 依存関係のインストール
  1. DockerでPostgreSQLデータベースを開始する
  1. データベースマイグレーションの適用
  1. プロジェクトを開始する

: ステップ4では、Prisma Clientの生成とデータベースへのシードも行われます。

これで、https://:3000/api/でAPIドキュメントにアクセスできるようになります。

プロジェクト構造とファイル

クローンしたリポジトリは以下の構造になっているはずです

: このフォルダにはtestディレクトリも含まれていることに気づくかもしれません。テストはこのチュートリアルでは扱いません。しかし、Prismaを使ったアプリケーションのテストのベストプラクティスについて学びたい場合は、このチュートリアルシリーズを確認してください: Prismaを使ったテストの究極ガイド

このリポジトリで注目すべきファイルとディレクトリは次のとおりです

  • srcディレクトリにはアプリケーションのソースコードが含まれています。3つのモジュールがあります。
    • appモジュールはsrcディレクトリのルートにあり、アプリケーションのエントリーポイントです。Webサーバーの起動を担当します。
    • prismaモジュールには、データベースへのインターフェースであるPrisma Clientが含まれています。
    • articlesモジュールは、/articlesルートのエンドポイントと、それに付随するビジネスロジックを定義します。
    • usersモジュールは、/usersルートのエンドポイントと、それに付随するビジネスロジックを定義します。
  • prismaフォルダには以下が含まれています
    • schema.prismaファイルはデータベーススキーマを定義します。
    • migrationsディレクトリにはデータベースのマイグレーション履歴が含まれています。
    • seed.tsファイルには、開発用データベースにダミーデータをシードするためのスクリプトが含まれています。
  • docker-compose.ymlファイルは、PostgreSQLデータベース用のDockerイメージを定義します。
  • .envファイルには、PostgreSQLデータベースの接続文字列が含まれています。

: これらのコンポーネントの詳細については、本チュートリアルシリーズの第1章をご覧ください。

REST APIに認証を実装する

このセクションでは、REST APIの認証ロジックの大部分を実装します。このセクションの終わりには、以下のエンドポイントが認証によって保護されます 🔒

  • GET /users
  • GET /users/:id
  • PATCH /users/:id
  • DELETE /users/:id

Webで使われる認証には、主に*セッションベース*認証と*トークンベース*認証の2種類があります。このチュートリアルでは、JSON Web Tokens (JWT)を使用したトークンベース認証を実装します。

: この短いビデオでは、両方の種類の認証の基本を説明しています。

まず、アプリケーションに新しいauthモジュールを作成します。新しいモジュールを生成するには、以下のコマンドを実行します。

いくつかのCLIプロンプトが表示されます。質問に適切に回答してください。

  1. このリソースにどのような名前を使いますか (複数形, 例: "users")? auth
  2. どのトランスポート層を使用しますか? REST API
  3. CRUDエントリポイントを生成しますか? いいえ

これで、src/authディレクトリに新しいauthモジュールがあるはずです。

passportのインストールと設定

passportは、Node.jsアプリケーション向けの一般的な認証ライブラリです。高い設定自由度を持ち、幅広い認証戦略をサポートしています。NestJSが構築されているWebフレームワークであるExpressと共に使用することを意図しています。NestJSには、@nestjs/passportというpassportとのファーストパーティ統合があり、NestJSアプリケーションでの使用を容易にします。

以下のパッケージをインストールして開始します。

必要なパッケージをインストールしたので、アプリケーションでpassportを設定できます。src/auth.module.tsファイルを開き、以下のコードを追加します。

@nestjs/passportモジュールは、アプリケーションにインポートできるPassportModuleを提供します。PassportModulepassportライブラリのラッパーであり、NestJS固有のユーティリティを提供します。PassportModuleの詳細については、公式ドキュメントで確認できます。

また、JWTの生成と検証に使用するJwtModuleも設定しました。JwtModulejsonwebtokenライブラリのラッパーです。secretはJWTの署名に使用される秘密鍵を提供します。expiresInオブジェクトはJWTの有効期限を定義します。現在は5分に設定されています。

: 以前のトークンが期限切れになった場合は、新しいトークンを生成することを忘れないでください。

コードスニペットに示されているjwtSecretを使用するか、OpenSSLを使用して独自のものを生成できます。

: 実際のアプリケーションでは、秘密鍵を直接コードベースに保存してはいけません。NestJSは、環境変数から秘密鍵をロードするための@nestjs/configパッケージを提供しています。詳細については、公式ドキュメントで確認できます。

POST /auth/login エンドポイントの実装

POST /loginエンドポイントはユーザーの認証に使用されます。ユーザー名とパスワードを受け入れ、資格情報が有効な場合はJWTを返します。まず、リクエストボディの形状を定義するLoginDtoクラスを作成します。

src/auth/dtoディレクトリ内にlogin.dto.tsという新しいファイルを作成します。

さて、emailpasswordフィールドを持つLoginDtoクラスを定義します。

また、JWTペイロードの形状を記述する新しいAuthEntityを定義する必要があります。src/auth/entityディレクトリ内にauth.entity.tsという新しいファイルを作成します。

さて、このファイルにAuthEntityを定義します。

AuthEntityには、JWTを格納するaccessTokenという単一の文字列フィールドしかありません。

さて、AuthService内に新しいloginメソッドを作成します。

loginメソッドは、まず指定されたメールアドレスでユーザーをフェッチします。ユーザーが見つからない場合は、NotFoundExceptionをスローします。ユーザーが見つかった場合は、パスワードが正しいかを確認します。パスワードが正しくない場合は、UnauthorizedExceptionをスローします。パスワードが正しい場合は、ユーザーのIDを含むJWTを生成して返します。

さて、AuthController内にPOST /auth/loginメソッドを作成します。

これで、APIに新しいPOST /auth/loginエンドポイントが作成されました。

https://:3000/apiページにアクセスし、POST /auth/loginエンドポイントを試してみてください。シードスクリプトで作成したユーザーの資格情報を提供します。

以下のリクエストボディを使用できます。

リクエストを実行した後、レスポンスとしてJWTが返されるはずです。

POST /auth/login endpoint

次のセクションでは、このトークンを使用してユーザーを認証します。

JWT認証戦略の実装

Passportにおいて、ストラテジーは、認証メカニズムを実装することでリクエストを認証する役割を担います。このセクションでは、ユーザー認証に使用されるJWT認証ストラテジーを実装します。

passportパッケージを直接使用するのではなく、内部でpassportパッケージを呼び出すラッパーパッケージ@nestjs/passportと連携します。@nestjs/passportでストラテジーを設定するには、PassportStrategyクラスを拡張するクラスを作成する必要があります。このクラスでは主に2つのことを行います。

  1. コンストラクタでsuper()メソッドにJWT戦略固有のオプションと設定を渡します。
  2. JWTペイロードに基づいてデータベースと連携し、ユーザーをフェッチするvalidate()コールバックメソッドです。ユーザーが見つかった場合、validate()メソッドはユーザーオブジェクトを返すことが期待されます。

まず、src/auth/strategyディレクトリ内にjwt.strategy.tsという新しいファイルを作成します。

さて、JwtStrategyクラスを実装します。

PassportStrategyクラスを拡張するJwtStrategyクラスを作成しました。PassportStrategyクラスは2つの引数を取ります。戦略の実装と戦略の名前です。ここでは、passport-jwtライブラリの事前定義された戦略を使用しています。

コンストラクタのsuper()メソッドにいくつかのオプションを渡しています。jwtFromRequestオプションは、リクエストからJWTを抽出するために使用できるメソッドを期待します。この場合、APIリクエストのAuthorizationヘッダーでベアラートークンを提供する標準的なアプローチを使用します。secretOrKeyオプションは、JWTの検証に使用する秘密鍵を戦略に伝えます。他にも多くのオプションがあり、それらはpassport-jwtリポジトリで確認できます。

passport-jwtの場合、PassportはまずJWTの署名を検証し、JSONをデコードします。デコードされたJSONはvalidate()メソッドに渡されます。JWT署名の仕組み上、以前にアプリによって署名・発行された有効なトークンを受け取ることが保証されます。validate()メソッドはユーザーオブジェクトを返すことが期待されます。ユーザーが見つからない場合、validate()メソッドはエラーをスローします。

: Passportはかなり紛らわしい場合があります。Passportを、認証プロセスをいくつかのステップに抽象化し、戦略と設定オプションでカスタマイズできるそれ自体がミニフレームワークであると考えると役立ちます。NestJSでPassportを使用する方法の詳細については、NestJS Passportレシピを読むことをお勧めします。

新しいJwtStrategyAuthModuleのプロバイダーとして追加します。

これで、JwtStrategyは他のモジュールから使用できるようになります。また、JwtStrategyクラスでUsersServiceが使用されているため、UsersModuleimportsに追加しました。

JwtStrategyクラスでUsersServiceにアクセスできるようにするには、UsersModuleexportsにも追加する必要があります。

JWT認証ガードの実装

ガードは、リクエストの処理を許可するかどうかを決定するNestJSの構造です。このセクションでは、認証が必要なルートを保護するために使用されるカスタムJwtAuthGuardを実装します。

src/authディレクトリ内にjwt-auth.guard.tsという新しいファイルを作成します。

さて、JwtAuthGuardクラスを実装します。

AuthGuardクラスは戦略の名前を期待します。この場合、前のセクションで実装したjwtという名前のJwtStrategyを使用しています。

これで、このガードをデコレーターとして使用してエンドポイントを保護できます。UsersController内のルートにJwtAuthGuardを追加します。

これらのエンドポイントのいずれかに認証なしでクエリを試みても、動作しなくなります。

`GET /users endpoint gives 401 response

Swaggerへの認証統合

現在、これらのエンドポイントが認証によって保護されているというSwagger上の表示はありません。認証が必要であることを示すために、コントローラーに@ApiBearerAuth()デコレーターを追加できます。

これで、認証によって保護されたエンドポイントにはSwaggerで鍵のアイコンが表示されるはずです 🔓

Auth protected endpoints in Swagger

現在、Swaggerで直接「認証」してこれらのエンドポイントをテストすることはできません。これを行うには、main.tsSwaggerModuleセットアップに.addBearerAuth()メソッド呼び出しを追加します。

これで、SwaggerのAuthorizeボタンをクリックしてトークンを追加できます。Swaggerはあなたのリクエストにトークンを追加するため、保護されたエンドポイントにクエリを実行できます。

: 有効なemailpasswordを使用して/auth/loginエンドポイントにPOSTリクエストを送信することでトークンを生成できます。

自分で試してみてください。

Authentication workflow in Swagger

パスワードのハッシュ化

現在、User.passwordフィールドはプレーンテキストで保存されています。これは、データベースが侵害された場合にすべてのパスワードも侵害されるため、セキュリティ上のリスクです。これを解決するには、パスワードをデータベースに保存する前にハッシュ化することができます。

パスワードをハッシュ化するには、bcrypt暗号化ライブラリを使用できます。npmでインストールします。

まず、UsersServicecreateメソッドとupdateメソッドを更新して、パスワードをデータベースに保存する前にハッシュ化します。

bcrypt.hash関数は2つの引数を受け取ります。ハッシュ関数への入力文字列と、ハッシュ化のラウンド数(コストファクターとも呼ばれます)です。ハッシュ化のラウンド数を増やすと、ハッシュの計算にかかる時間が増加します。ここにはセキュリティとパフォーマンスのトレードオフがあります。ハッシュ化のラウンド数が多いほど、ハッシュの計算に時間がかかり、ブルートフォース攻撃の防止に役立ちます。ただし、ラウンド数が多いということは、ユーザーがログインする際にハッシュの計算により時間がかかることにもなります。このStack Overflowの回答には、このトピックに関する良い議論があります。

bcryptはまた、ハッシュに対するブルートフォース攻撃をより困難にするために、ソルト処理と呼ばれる別の手法を自動的に使用します。ソルト処理とは、ハッシュ化する前に入力文字列にランダムな文字列を追加する手法です。これにより、各パスワードが異なるソルト値を持つため、攻撃者は事前に計算されたハッシュのテーブルを使用してパスワードをクラックすることができなくなります。

データベースにパスワードを挿入する前にハッシュ化するように、データベースシードスクリプトも更新する必要があります。

npx prisma db seedでシードスクリプトを実行すると、データベースに保存されているパスワードがハッシュ化されていることが確認できます。

毎回異なるソルト値が使用されるため、passwordフィールドの値は異なります。重要なのは、値がハッシュ化された文字列になっていることです。

さて、正しいパスワードでloginを使用しようとすると、HTTP 401エラーに直面します。これは、loginメソッドがユーザーリクエストからのプレーンテキストパスワードとデータベース内のハッシュ化されたパスワードを比較しようとするためです。loginメソッドを更新してハッシュ化されたパスワードを使用するようにします。

これで、正しいパスワードでログインし、レスポンスとしてJWTを取得できます。

まとめと最終的な注意点

この章では、NestJS REST APIにJWT認証を実装する方法を学びました。また、パスワードのソルト処理やSwaggerとの認証統合についても学びました。

このチュートリアルの完成したコードは、GitHubリポジトリend-authenticationブランチにあります。問題に気づいた場合は、お気軽にリポジトリでissueを立てるか、PRを提出してください。Twitterで直接私に連絡することもできます。

次の投稿をお見逃しなく!

Prismaニュースレターに登録する

© . All rights reserved.