NestJS、Prisma、PostgreSQLでREST APIを構築するシリーズの5番目のチュートリアルへようこそ!このチュートリアルでは、NestJS REST APIにJWT認証を実装する方法を学びます。
目次
はじめに
このシリーズの前の章では、NestJS REST APIでリレーショナルデータを処理する方法を学びました。User
モデルを作成し、User
モデルとArticle
モデル間に一対多のリレーションシップを追加しました。また、User
モデルのCRUDエンドポイントを実装しました。
この章では、Passportと呼ばれるパッケージを使用してAPIに認証を追加する方法を学びます。
- まず、Passportと呼ばれるライブラリを使用して、JSON Web Token(JWT)ベースの認証を実装します。
- 次に、bcryptライブラリを使用してパスワードをハッシュ化し、データベースに保存されているパスワードを保護します。
このチュートリアルでは、前章で構築したAPIを使用します。
開発環境
このチュートリアルを進めるには、以下が必要です。
- ... Node.jsがインストールされていること。
- ... DockerとDocker 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
ブランチをチェックアウトします。
開始するには、次の操作を実行します。
- クローンしたディレクトリに移動します
- 依存関係をインストールします
- DockerでPostgreSQLデータベースを起動します
- データベースの移行を適用します
- プロジェクトを開始します
注:ステップ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プロンプトがいくつか表示されます。質問に適切に答えてください。
このリソースに使用する名前(複数形、例:「users」)は何ですか?
authどのトランスポート層を使用しますか?
REST APICRUDエントリポイントを生成しますか?
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を取得できるはずです。
次のセクションでは、このトークンを使用してユーザーを認証します。
JWT認証ストラテジーを実装する
Passportでは、ストラテジーはリクエストの認証を担当し、認証メカニズムを実装することでそれを実現します。このセクションでは、ユーザーの認証に使用されるJWT認証ストラテジーを実装します。
passport
パッケージを直接使用するのではなく、ラッパーパッケージ@nestjs/passport
と対話します。これは、バックグラウンドでpassport
パッケージを呼び出します。@nestjs/passport
でストラテジーを構成するには、PassportStrategy
クラスを拡張するクラスを作成する必要があります。このクラスでは、主に次の2つのことを行う必要があります。
- コンストラクターの
super()
メソッドに、JWTストラテジー固有のオプションと構成を渡します。 - 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レシピを読むことをお勧めします。
新しいJwtStrategy
をAuthModule
のプロバイダーとして追加します。
これで、JwtStrategy
を他のモジュールで使用できるようになりました。JwtStrategy
クラスでUsersService
が使用されているため、imports
にUsersModule
も追加しました。
UsersService
をJwtStrategy
クラスでアクセス可能にするには、UsersModule
のexports
にも追加する必要があります。
JWT認証ガードを実装する
ガードは、リクエストの続行を許可するかどうかを決定するNestJSの構成要素です。このセクションでは、認証を必要とするルートを保護するために使用されるカスタムJwtAuthGuard
を実装します。
src/auth
ディレクトリ内にjwt-auth.guard.ts
という名前の新しいファイルを作成します。
次に、JwtAuthGuard
クラスを実装します。
AuthGuard
クラスは、ストラテジーの名前を期待します。この場合、前のセクションで実装したJwtStrategy
を使用しています。これはjwt
という名前です。
これで、このガードをデコレータとして使用してエンドポイントを保護できます。UsersController
のルートにJwtAuthGuard
を追加します。
認証なしでこれらのエンドポイントのいずれかをクエリしようとすると、機能しなくなります。
Swaggerに認証を統合する
現在、Swaggerにはこれらのエンドポイントが認証保護されているという表示はありません。@ApiBearerAuth()
デコレータをコントローラーに追加して、認証が必要であることを示すことができます。
これで、認証保護されたエンドポイントには、Swaggerにロックアイコン🔓が表示されるはずです。
現在、Swaggerで直接「認証」してこれらのエンドポイントをテストすることはできません。これを行うには、main.ts
のSwaggerModule
設定に.addBearerAuth()
メソッド呼び出しを追加できます。
これで、SwaggerのAuthorizeボタンをクリックしてトークンを追加できます。Swaggerはリクエストにトークンを追加するため、保護されたエンドポイントをクエリできます。
注:有効な
password
を使用して/auth/login
エンドポイントにPOST
リクエストを送信することで、トークンを生成できます。
自分で試してみてください。
パスワードのハッシュ化
現在、User.password
フィールドはプレーンテキストで保存されています。これは、データベースが侵害された場合、すべてのパスワードも侵害されるため、セキュリティリスクです。これを修正するには、データベースに保存する前にパスワードをハッシュ化できます。
bcrypt
暗号化ライブラリを使用してパスワードをハッシュ化できます。npm
でインストールします。
まず、UsersService
のcreate
メソッドと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ニュースレターに登録する