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拡張機能がインストールされていること。Prisma VS Code拡張機能は、Prismaに非常に優れたIntelliSenseと構文強調表示を追加します。
  • ... オプションでこのシリーズで提供されるコマンドを実行するために、Unixシェル(LinuxおよびmacOSのターミナル/シェルなど)にアクセスできること。

Unixシェルがない場合(たとえば、Windowsマシンを使用している場合)でも、進めることはできますが、シェルコマンドをマシンに合わせて変更する必要がある場合があります。

リポジトリのクローン

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

このチュートリアルの開始点は、end-validationブランチのGitHubリポジトリで入手できます。開始するには、リポジトリをクローンし、end-validationブランチをチェックアウトします。

開始するには、次の操作を実行します。

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

:ステップ4では、Prisma Clientも生成され、データベースがシードされます。

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

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

クローンしたリポジトリは、次の構成になっているはずです。

:このフォルダにはtestディレクトリも付属していることに気付くかもしれません。テストはこのチュートリアルでは扱いません。ただし、Prismaを使用したアプリケーションのテストのベストプラクティスについて学習したい場合は、このチュートリアルシリーズ「The Ultimate Guide to Testing with 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エントリポイントを生成しますか? No

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

passportをインストールして構成する

passportは、Node.jsアプリケーション向けの一般的な認証ライブラリです。高度に構成可能で、幅広い認証ストラテジーをサポートしています。NestJSが基盤としているExpress Webフレームワークで使用するように設計されています。NestJSには、NestJSアプリケーションで簡単に使用できるようにする@nestjs/passportと呼ばれるpassportとのファーストパーティ統合があります。

まず、次のパッケージをインストールします。

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

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

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

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

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

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

POST /auth/loginエンドポイントを実装する

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

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

次に、emailフィールドとpasswordフィールドを持つ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エンドポイントができたはずです。

http://localhost:3000/apiページに移動し、POST /auth/loginエンドポイントを試してください。シードスクリプトで作成したユーザーのクレデンシャルを入力します。

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

リクエストを実行すると、レスポンスでJWTを取得できるはずです。

POST /auth/login endpoint

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

JWT認証ストラテジーを実装する

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

passportパッケージを直接使用するのではなく、ラッパーパッケージ@nestjs/passportと対話します。これは、バックグラウンドで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が使用されているため、importsUsersModuleも追加しました。

UsersServiceJwtStrategyクラスでアクセス可能にするには、UsersModuleexportsにも追加する必要があります。

JWT認証ガードを実装する

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

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

次に、JwtAuthGuardクラスを実装します。

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

これで、このガードをデコレータとして使用してエンドポイントを保護できます。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との認証統合についても学びました。

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

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

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