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 Extensionがインストールされていること。Prisma VS Code拡張機能は、Prismaに非常に優れたIntelliSenseとシンタックスハイライトを追加します。
- ... *オプションとして*、このシリーズで提供されるコマンドを実行するために、Unixシェル(LinuxやmacOSのターミナル/シェルなど)にアクセスできること。
Unixシェルをお持ちでない場合(例えば、Windowsマシンをご利用の場合)でも、このチュートリアルを進めることは可能ですが、シェルコマンドをマシンに合わせて変更する必要があるかもしれません。
リポジトリのクローン
このチュートリアルの出発点は、本シリーズの第2章の終了時点です。NestJSで構築された基本的なREST APIが含まれています。
このチュートリアルの出発点は、GitHubリポジトリのend-validation
ブランチで利用可能です。開始するには、リポジトリをクローンし、end-validation
ブランチをチェックアウトしてください。
さて、始めるために以下の操作を行ってください
- クローンしたディレクトリに移動する
- 依存関係のインストール
- DockerでPostgreSQLデータベースを開始する
- データベースマイグレーションの適用
- プロジェクトを開始する
注: ステップ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プロンプトが表示されます。質問に適切に回答してください。
このリソースにどのような名前を使いますか (複数形, 例: "users")?
authどのトランスポート層を使用しますか?
REST APICRUDエントリポイントを生成しますか?
いいえ
これで、src/auth
ディレクトリに新しいauth
モジュールがあるはずです。
passport
のインストールと設定
passport
は、Node.jsアプリケーション向けの一般的な認証ライブラリです。高い設定自由度を持ち、幅広い認証戦略をサポートしています。NestJSが構築されているWebフレームワークであるExpressと共に使用することを意図しています。NestJSには、@nestjs/passport
というpassport
とのファーストパーティ統合があり、NestJSアプリケーションでの使用を容易にします。
以下のパッケージをインストールして開始します。
必要なパッケージをインストールしたので、アプリケーションでpassport
を設定できます。src/auth.module.ts
ファイルを開き、以下のコードを追加します。
@nestjs/passport
モジュールは、アプリケーションにインポートできるPassportModule
を提供します。PassportModule
はpassport
ライブラリのラッパーであり、NestJS固有のユーティリティを提供します。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
エンドポイントが作成されました。
https://:3000/api
ページにアクセスし、POST /auth/login
エンドポイントを試してみてください。シードスクリプトで作成したユーザーの資格情報を提供します。
以下のリクエストボディを使用できます。
リクエストを実行した後、レスポンスとしてJWTが返されるはずです。
次のセクションでは、このトークンを使用してユーザーを認証します。
JWT認証戦略の実装
Passportにおいて、ストラテジーは、認証メカニズムを実装することでリクエストを認証する役割を担います。このセクションでは、ユーザー認証に使用されるJWT認証ストラテジーを実装します。
passport
パッケージを直接使用するのではなく、内部でpassport
パッケージを呼び出すラッパーパッケージ@nestjs/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
が使用されているため、UsersModule
もimports
に追加しました。
JwtStrategy
クラスでUsersService
にアクセスできるようにするには、UsersModule
のexports
にも追加する必要があります。
JWT認証ガードの実装
ガードは、リクエストの処理を許可するかどうかを決定するNestJSの構造です。このセクションでは、認証が必要なルートを保護するために使用されるカスタムJwtAuthGuard
を実装します。
src/auth
ディレクトリ内にjwt-auth.guard.ts
という新しいファイルを作成します。
さて、JwtAuthGuard
クラスを実装します。
AuthGuard
クラスは戦略の名前を期待します。この場合、前のセクションで実装したjwt
という名前のJwtStrategy
を使用しています。
これで、このガードをデコレーターとして使用してエンドポイントを保護できます。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との認証統合についても学びました。
このチュートリアルの完成したコードは、GitHubリポジトリのend-authentication
ブランチにあります。問題に気づいた場合は、お気軽にリポジトリでissueを立てるか、PRを提出してください。Twitterで直接私に連絡することもできます。
次の投稿をお見逃しなく!
Prismaニュースレターに登録する