シリーズの3回目となる今回は、トークンストレージにPrismaを使用してREST APIをパスワードレス認証で保護し、認可を実装する方法を見ていきます。
はじめに
このシリーズの目標は、具体的な問題を解決することで、最新のバックエンドのためのさまざまなパターン、問題、アーキテクチャを探求し、実証することです。その具体的な問題とは、オンラインコースの採点システムです。これは、多様なリレーションタイプを備え、現実世界のユースケースを表現するのに十分な複雑さを持っているため、良い例となります。
ライブストリームの録画は上記で利用可能であり、この記事と同じ内容をカバーしています。
シリーズで扱う内容
このシリーズでは、バックエンド開発のあらゆる側面におけるデータベースの役割に焦点を当て、以下をカバーします。
トピック | パート |
---|---|
データモデリング | パート1 |
CRUD | パート1 |
集計 | パート1 |
REST APIレイヤー | パート2 |
バリデーション | パート2 |
テスト | パート2 |
パスワードレス認証 | パート3 (現在) |
認可 | パート3 (現在) |
外部APIとの統合 | パート3 (現在) |
デプロイメント | 今後の予定 |
今日学ぶこと
最初の記事では、問題領域のデータモデルを設計し、Prisma Clientを使用してデータをデータベースに保存するシードスクリプトを作成しました。
シリーズの2番目の記事では、最初の記事のデータモデルとPrismaスキーマの上にREST APIを構築しました。Hapiを使用してREST APIを構築し、HTTPリクエストを介してリソースに対してCRUD操作を実行できるようにしました。
シリーズの3番目の記事では、認証と認可の背後にある概念、2つの違い、そしてJSON Web Tokens (JWT)とHapiを使用して、REST APIを保護するためのメールベースのパスワードレス認証と認可を実装する方法について学びます。
具体的には、以下の側面を開発します。
- パスワードレス認証: 一意のトークンを含むメールを送信することでログインおよびサインアップする機能を追加します。ユーザーは、メールで受信したトークンをAPIに送信し、認証を必要とするAPIエンドポイントへのアクセスを許可する長期的なJWTトークンを取得することで、認証プロセスを完了します。
- 認可: ユーザーがアクセスおよび操作できるリソースを制限する認可ロジックを追加します。
この記事の終わりまでに、REST APIはRESTエンドポイントにアクセスするための認証で保護されます。さらに、Hapiのpre
ルートオプションを使用して、エンドポイントのサブセットに認可ルールを追加し、特定のユーザーの権限に応じてアクセスを許可します。すべてのエンドポイントの認可ルールを含むAPIは、このGitHubリポジトリで利用できます。
注: ガイド全体を通して、ステップを正しく実行したかどうかを検証できるさまざまなチェックポイントがあります。
前提条件
前提知識
このシリーズは、TypeScript、Node.js、およびリレーショナルデータベースの基本的な知識を前提としています。JavaScriptの経験はあるが、TypeScriptを試したことがない場合でも、進めることができます。このシリーズではPostgreSQLを使用します。ただし、ほとんどの概念はMySQLなどの他のリレーショナルデータベースにも適用できます。RESTの概念に精通していると役立ちます。それ以外には、Prismaに関する事前の知識は必要ありません。シリーズでそれをカバーするためです。
開発環境
以下がインストールされている必要があります。
Visual Studio Codeを使用している場合は、Prisma拡張機能が、構文の強調表示、フォーマット、およびその他のヘルパーのために推奨されます。
注:Dockerを使用したくない場合は、ローカルPostgreSQLデータベースまたはHerokuでホストされたPostgreSQLデータベースを設定できます。
外部サービス
バックエンドからパスワードレス認証メールを送信できるようにするには、SendGridアカウントが必要です。SendGridは、1日に最大100通のメールを送信できる無料プランを提供しています。
サインアップしたら、SendGridコンソールのAPIキーに移動し、APIキーを生成して安全な場所に保管してください。
リポジトリのクローン
シリーズのソースコードはGitHubにあります。
開始するには、リポジトリをクローンして依存関係をインストールします。
注:
part-3
ブランチをチェックアウトすると、記事と同じ開始点から始めることができます。
PostgreSQLの起動
PostgreSQLを起動するには、real-world-grading-app
フォルダーから次のコマンドを実行します。
注: Dockerは、
docker-compose.yml
ファイルを使用してPostgreSQLコンテナを起動します。
認証と認可の概念
実装に入る前に、認証と認可に関連するいくつかの概念を見ていきましょう。
2つの用語はしばしば互換的に使用されますが、認証と認可は異なる目的を果たします。一般的に言えば、これらは補完的な方法でアプリケーションを保護するために両方とも使用されます。
簡単に言えば、認証はユーザーが誰であるかを検証するプロセスであり、認可はユーザーが何にアクセスできるかを検証するプロセスです。
現実世界の認証の一例は、有効なパスポートです。公式文書(偽造が困難な文書)に写っている人物に似ているという事実は、あなたが主張する人物であることを認証します。たとえば、空港に行くとき、パスポートを提示すると、セキュリティを通過できるようになります。
同じ例で、認可はフライトへの搭乗を許可されるプロセスです。搭乗券(通常はスキャンされ、フライト乗客のデータベースと照合されます)を提示すると、地上係員がフライトへの搭乗を許可します。
Webアプリケーションでの認証
Webアプリケーションは通常、ユーザー名とパスワードを使用してユーザーを認証します。有効なユーザー名とパスワードが渡されると、パスワードはあなたとアプリケーションのみが知っているはずであるため、アプリケーションはあなたが主張するユーザーであることを検証できます。
注: ユーザー名/パスワード認証を使用するWebアプリケーションは、データベースにパスワードをクリアテキストで保存することはめったにありません。代わりに、ハッシュと呼ばれる手法を使用して、パスワードのハッシュを保存します。これにより、バックエンドはパスワードを知らなくても検証できます。
ハッシュ関数は、任意の入力を受け取り、同じ入力が与えられた場合、常に同じ固定長の文字列/数値を生成する数学関数です。ハッシュ関数の力は、パスワードからハッシュに移動できますが、ハッシュからパスワードに移動できないことにあります。
これにより、実際のパスワードを保存せずに、ユーザーが送信したパスワードを検証できます。パスワードハッシュを保存すると、ハッシュ化されたパスワードでログインすることは不可能であるため、データベースへのアクセスが侵害された場合にユーザーを保護できます。
近年、Webセキュリティは、侵害された重要なWebサイトの数が増加しているため、ますます懸念事項になっています。この傾向は、多要素認証などのより安全な認証アプローチを導入することにより、セキュリティへのアプローチ方法に影響を与えました。
多要素認証は、ユーザーが2つ以上の証拠(要素とも呼ばれる)を提示することに成功した後、認証される認証方法です。たとえば、ATMからお金を引き出す場合、銀行カードの所持とPINコードという2つの認証要素が必要です。
Webアプリケーションの場合、カードの所持を検証するのは難しいため、多要素認証は、ユーザー名/パスワードを認証アプリ(スマートフォンまたはこれらのパスワードを生成する特別なデバイスにインストールされたアプリ)によって生成されたワンタイムトークンで補完することによって実装されることがよくあります。
この記事では、ユーザーエクスペリエンスとセキュリティを向上させる2段階のアプローチであるメールベースのパスワードレス認証を実装します。これは、ログインを試みるときに、秘密トークンをユーザーのメールアカウントに送信することで機能します。ユーザーがメールを開き、トークンをアプリケーションに渡すと、アプリケーションはユーザーを認証し、ユーザーがメールアカウントの所有者であることを確信できます。
このアプローチは、ユーザーのメールサービスに依存しており、ユーザーをすでに認証していると想定できます。ユーザーがパスワードを設定して覚えておく必要がないため、ユーザーエクスペリエンスが向上します。アプリケーションが攻撃対象となる可能性のあるパスワード管理の責任から解放されるため、セキュリティが強化されます。
認証をユーザーのメールアカウントにアウトソーシングするということは、アプリケーションがユーザーのメールアカウントのセキュリティの利点と弱点を継承することを意味します。しかし、最近では、ほとんどのメールサービスが2要素認証やその他のセキュリティ対策のオプションを提供しています。
それでも、このアプローチは、ユーザーが弱いパスワードを選択したり、複数のWebサイトで再利用したりすることを回避します。パスワードを完全に取り除くということは、これらのユーザーがより安全になることを意味します。推測したり、ブルートフォースアタックしたり、完全にクラックしたりできるパスワードはもうありません。
認証とサインアップ/ログインフロー
メールベースのパスワードレス認証は、2つのトークンタイプを含む2段階のプロセスです。
認証フローは次のようになります。
- ユーザーは、認証プロセスを開始するために、ペイロードにメールアドレスを含めてAPIの
/login
エンドポイントを呼び出します。 - メールアドレスが新しい場合、ユーザーはUserテーブルに作成されます。
- メールトークンがバックエンドによって生成され、Tokenテーブルに保存されます。
- メールトークンがユーザーのメールに送信されます。
- ユーザーは、(メールで受信した)メールトークンとメールアドレスを
/authenticate
エンドポイントに送信します。 - バックエンドは、ユーザーから送信されたメールトークンを検証します。有効で、トークンが期限切れになっていない場合、JWTトークンが生成され、Tokenテーブルに保存されます。
- JWTトークンは、
Authorization
ヘッダーを介してユーザーに返送されます。
トークンタイプは2つあります。
- メールトークン: 8桁の数値トークンで、たとえば10分などの短期間有効であり、ユーザーのメールに送信されます。トークンの唯一の目的は、ユーザーがメールに関連付けられていることを検証することです。つまり、グレーディングアプリ関連のエンドポイントへのアクセスを許可するものではありません。
- 認証トークン: ペイロードに
tokenId
を含むJWTトークン。このトークンは、APIへのリクエストを行うときにAuthorization
ヘッダーに渡すことで、保護されたエンドポイントにアクセスするために使用できます。トークンは、12時間有効であるという意味で長寿命です。
この認証戦略では、単一のエンドポイントがログインと登録の両方を処理します。ログインとサインアップの唯一の違いは、ユーザーがすでに存在する場合、「User」テーブルに行を作成するかどうかであるため、可能です。
JSON Web Tokens
JSON Web Tokens (JWT)は、2つの当事者間でクレームを安全に表現するためのオープンで標準的な方法です。この標準は、JSONオブジェクトとして当事者間で情報を安全に送信するためのコンパクトで自己完結型の方法を定義します。この情報は、デジタル署名されているため、検証および信頼できます。
JWTトークンには、Base64でエンコードされた3つの部分、ヘッダー、ペイロード、および署名が含まれており、次のように見えます(部分は.
で区切られています)。
注:Base64はデータを表現する別の方法です。暗号化は含まれていません。
上記のヘッダーとペイロードをBase64を使用してデコードすると、次のようになります。
- ヘッダー:
{"alg":"HS256","typ":"JWT"}
- ペイロード:
{"tokenId":9}
トークンの署名部分は、ヘッダー、ペイロード、およびシークレットを署名アルゴリズム(この場合はHS256)を介して渡すことによって作成されます。シークレットはバックエンドのみが知っており、トークンの信頼性を検証するために使用されます。
この記事では、JWTは長寿命の認証トークンに使用されます。トークンのペイロードには、データベースに保存され、トークンが作成されたユーザーを参照するtokenId
が含まれます。これにより、バックエンドは関連付けられたユーザーを見つけることができます。
注: このアプローチは、ステートフルJWTとして知られており、トークンはデータベースに保存されているセッションを参照します。これは、リクエストの認証にデータベースへのラウンドトリップが必要になることを意味し、リクエストの処理に必要な時間が増加しますが、このアプローチは、トークンをバックエンドから取り消すことができるため、より安全です。
Prismaスキーマへのトークンモデルの追加
リクエストが行われたときに検証できるように、トークンをデータベースに保存する必要があります。このステップでは、新しいToken
モデルをPrismaスキーマに追加し、いくつかのフィールドをオプションにするためにUser
モデルを更新します。
prisma/schema.prisma
にあるPrismaスキーマを開き、次のように更新します。
導入された変更を見ていきましょう。
connectOrCreate
およびtransactionApi
プレビュー機能を有効にします。これらは次のステップで使用されます。- Prisma 2.5.0以降で安定している
aggregateApi
プレビュー機能を削除します。 User
モデルでは、firstName
とlastName
がオプションになりました。これにより、ユーザーはメールアドレスのみでログイン/登録できます。- 新しい
Token
モデルが追加されました。各ユーザーは多数のトークンを持つことができ、関係は1対多になります。Token
モデルには、有効期限、2つのトークンタイプ(TokenType
列挙型を使用)、およびメールトークンのストレージに対応する関連フィールドが含まれています。
データベーススキーマを移行するには、次のように移行を作成して実行します。
チェックポイント: 出力に次のようなものが表示されるはずです。
注:
prisma migrate dev
コマンドを実行すると、デフォルトでPrisma Clientも生成されます。
メール送信機能の追加
バックエンドはユーザーログイン時にメールを送信するため、アプリケーションの残りの部分にメール送信機能を公開するプラグインを作成します。Hapiプラグインは、Prismaプラグインと同様の規約に従います。
この記事では、SendGridとSendGrid APIとの簡単な統合のために@sendgrid/mail
npmパッケージを使用します。
依存関係の追加
メールプラグインの作成
src/plugins/
フォルダーにemail.ts
という名前の新しいファイルを作成します。
そして、ファイルに次を追加します。
プラグインは、ルートハンドラー全体でアクセス可能なserver.app
オブジェクトにsendEmailToken
関数を公開します。これは、SendGridコンソールからのキーを使用して、本番環境で設定するSENDGRID_API_KEY
環境変数を使用します。開発中は、設定を解除したままにすることができ、トークンはメールで送信される代わりにログに記録されます。
最後に、server.ts
でプラグインを登録します。
Hapiによる認証の追加
認証を実装するために、最初に/login
および/register
ルートを定義します。これらは、データベースでのユーザーとトークンの作成、メールトークンの送信、メールの検証、およびJWT認証トークンの生成を処理します。2つのエンドポイントが認証プロセスを処理しますが、APIを保護するものではないことに注意してください。
APIを保護するために、2つのルートが定義されたら、hapi-auth-jwt2
ライブラリによって提供されるjwt
スキームを使用する認証戦略を定義します。
注: Hapiの認証は、スキームと戦略の概念に基づいています。スキームは認証を処理する方法ですが、戦略はスキームの事前設定されたインスタンスです。この記事では、
jwt
認証スキームに基づいて戦略を定義するだけで済みます。
これらすべてのロジックをauth
プラグインにカプセル化します。
依存関係の追加
最初に、次の依存関係をプロジェクトに追加します。
認証プラグインの作成
次に、認証ロジックをカプセル化する認証プラグインを作成します。
src/plugins/
フォルダーにauth.ts
という名前の新しいファイルを作成します。
そして、ファイルに次を追加します。
注: 認証プラグインは、
prisma
、hapi-auth-jwt2
、およびapp/email
プラグインへの依存関係を定義します。prismaプラグインは、シリーズのパート2で定義されており、Prisma Clientにアクセスするために使用されます。hapi-auth-jwt2
プラグインは、認証戦略を定義するために使用するjwt
認証スキームを定義します。最後に、app/email
はsendEmailToken
関数にアクセスできるようにします。
ログインエンドポイントの定義
authPlugin
のregister
関数で、次のように新しいログインルートを定義します。
注:
options.auth
はfalseに設定されています。これにより、デフォルトの認証戦略を設定すると、明示的に無効にしないすべてのルートで認証が必要になるため、エンドポイントは開いたままになります。
プラグインのregister関数の外側に、次を追加します。
loginHandler
は次のことを行います。
- メールアドレスはリクエストペイロードから取得されます。
- トークンが生成され、データベースに保存されます。
connectOrCreate
を使用すると、ペイロードのメールアドレスを持つユーザーが存在しない場合は、作成されます。それ以外の場合は、既存のユーザーへのリレーションが作成されます。- トークンはペイロードのメールアドレスに送信されます(または、
SENDGRID_API_KEY
が設定されていない場合はコンソールに記録されます)。
最後に、server.ts
でプラグインを登録します。
チェックポイント
npm run dev
でサーバーを起動します。- curlを使用して
/login
エンドポイントにPOST呼び出しを行います:curl --header "Content-Type: application/json" --request POST --data '{"email":"test@test.io"}' localhost:3000/login
。バックエンドからトークンがログに記録されるはずです:test@test.ioのメールトークン:27948216
認証エンドポイントの定義
現時点では、バックエンドはユーザーを作成し、メールトークンを生成し、メールで送信できます。ただし、生成されたトークンはまだ機能していません。次に、/authenticate
エンドポイントを作成し、データベースに対してメールトークンを検証し、ユーザーに長寿命のJWT認証トークンをauthorization
ヘッダーで返すことで、認証の2番目のステップを実装します。
最初に、次のルート宣言をauthPlugin
に追加します。
ルートには、email
とemailToken
の両方が必要です。ログインを試みる正当なユーザーのみが両方を知っているため、email
とemailToken
の両方を推測することはより困難になり、8桁の数字を推測するブルートフォースアタックのリスクを軽減します。
次に、auth.ts
に次を追加します。
注: 環境変数
JWT_SECRET
は、次のコマンドを実行することで生成できます:node -e "console.log(require('crypto').randomBytes(256).toString('base64'));"
。これは常に本番環境で設定する必要があります。
ハンドラーは、データベースからメールトークンをフェッチし、有効であることを確認し、データベースに新しいAPIトークンを作成し、(データベース内のトークンへの参照を使用して)JWTトークンを生成し、メールトークンを無効にし、Authorization
ヘッダーでトークンを返します。
チェックポイント
npm run dev
でサーバーを起動します。/login
エンドポイントにcurlを使用してPOST呼び出しを行います:curl --header "Content-Type: application/json" --request POST --data '{"email":"test@test.io"}' localhost:3000/login
。バックエンドからトークンがログに記録されるはずです:test@test.ioのメールトークン:13080740
。- そのトークンを取得し、curlで
/authenticate
エンドポイントを呼び出してください:curl -v --header "Content-Type: application/json" --request POST --data '{"email":"hello@prisma.io", "emailToken": "13080740"}' localhost:3000/authenticate
。 - レスポンスは
200
ステータスを持ち、次のようなAuthorization
ヘッダーを含むはずです:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg
認証ストラテジーの定義
認証ストラテジーは、認証を必要とするエンドポイントへのリクエストをHapiがどのように検証するかを定義します。このステップでは、JWTトークンを使用してリクエストを検証するロジックを定義します。具体的には、JWTトークン内のtokenId
を使用して、データベースからユーザーに関する情報を取得します。
認証ストラテジーを定義するには、次のコードをauth.ts
に追加してください。
authPlugin.register
関数内に、以下を追加してください。
最後に、validateAPIToken
関数を追加してください。
validateAPIToken
関数は、API_AUTH_STATEGY
(前のステップでデフォルトとして設定したもの)を使用するすべてのルートの前に呼び出されます。
validateAPIToken
関数の目的は、リクエストの続行を許可するかどうかを決定することです。これは、isValid
とcredentials
を含むreturnオブジェクトによって行われます。
isValid
:トークンが正常に検証されたかどうかを決定します。credentials
は、ユーザーに関する情報をリクエストオブジェクトに渡すために使用できます。credentials
に渡されたオブジェクトは、ルートハンドラー内でrequest.auth.credentials
を介してアクセスできます。
このケースでは、トークンがデータベースに存在し、有効期限切れでない場合、有効であると判断します。その場合、ユーザーが教師であるコースを取得し(認可を実装するために使用されます)、それをtokenId
、userId
、およびisAdmin
とともにcredentialsオブジェクトに渡します。
ほとんどのエンドポイントは認証を必要としますが(デフォルトの認証ストラテジーのため)、まだ認可ルールはありません。つまり、GET /courses
エンドポイントにアクセスするには、Authorization
ヘッダーに有効なJWTトークンを含める必要があります。
チェックポイント
npm run dev
でサーバーを起動します。- curlで
/courses
エンドポイントにGETリクエストを送信してください:curl -v localhost:3000/courses
。次のレスポンスで401ステータスコードが表示されるはずです:{"statusCode":401,"error":"Unauthorized","message":"Missing authentication"}
。 - 次の例のように、最後のチェックポイントからのトークンを使用して
Authorization
ヘッダーを指定して再度呼び出してください:curl -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg" localhost:3000/courses
。リクエストは成功するはずです。
おめでとうございます。メールベースのパスワードレス認証を実装し、エンドポイントを保護することに成功しました。次に、認可ルールを定義します。
認可の追加
バックエンドの認可モデルは、ユーザーが何を実行できるかを定義します。言い換えれば、どのエンティティに対して操作を実行できるかを定義します。
ユーザーに権限を付与する主なプロパティは次のとおりです。
- ユーザーは管理者ですか(ユーザーモデルの
isAdmin
フィールドで示されるように)? そうであれば、すべての操作を実行できます。 - ユーザーはコースの教師ですか? そうであれば、ユーザーはテスト、テスト結果、および登録など、コース固有のすべてのリソースに対してCRUD操作を実行できます。
ユーザーが管理者でもコースの教師でもない場合でも、新しいコースの作成、既存のコースへの学生としての登録、テスト結果の取得、およびユーザープロフィールのフェッチと更新はできる必要があります。
注: このアプローチは、ロールベースとリソースベースの2つの認可アプローチを組み合わせています。コース登録から権限を導き出すことは、リソースベースの認可の一形態です。つまり、アクションは特定のリソースに基づいて認可されます。例:教師としてのコースへの登録により、ユーザーは関連するテストを作成し、テスト結果を提出できます。一方、管理者ユーザー(
isAdmin
がtrueに設定されている)へのアクションの認可は、ユーザーが「管理者」ロールを持つロールベースの認可の一形態です。
エンドポイントの認可ルール
提案された認可ルールを実装するために、まず、提案された認可ルールを含むエンドポイントのリストを見直します。
HTTPメソッド | ルート | 説明 | 認可ルール |
---|---|---|---|
POST | /login | ログイン/サインアップを開始し、メールトークンを送信する | オープン |
POST | /authenticate | ユーザーを認証し、JWTトークンを作成する | オープン(メールトークンが必要) |
GET | /profile | 認証済みユーザーのプロフィールを取得する | 認証済みユーザー |
POST | /users | ユーザーを作成する | 管理者のみ |
GET | /users/{userId} | ユーザーを取得する | 管理者または認証済みユーザー |
PUT | /users/{userId} | ユーザーを更新する | 管理者または認証済みユーザー |
DELETE | /users/{userId} | ユーザーを削除する | 管理者または認証済みユーザー |
GET | /users | ユーザーを取得する | 管理者のみ |
GET | /users/{userId}/courses | ユーザーのコース登録を取得する | 管理者または認証済みユーザー |
POST | /users/{userId}/courses | ユーザーをコースに登録する(学生または教師として) | 管理者または認証済みユーザー |
DELETE | /users/{userId}/courses/{courseId} | ユーザーのコース登録を削除する | 管理者または認証済みユーザー |
POST | /courses | コースを作成する | 認証済みユーザー |
GET | /courses | コースを取得する | 認証済みユーザー |
GET | /courses/{courseId} | コースを取得する | 認証済みユーザー |
PUT | /courses/{courseId} | コースを更新する | 管理者またはコースの教師のみ |
DELETE | /courses/{courseId} | コースを削除する | 管理者またはコースの教師のみ |
POST | /courses/{courseId}/tests | コースのテストを作成する | 管理者またはコースの教師のみ |
GET | /courses/tests/{testId} | テストを取得する | 認証済みユーザー |
PUT | /courses/tests/{testId} | テストを更新する | 管理者またはコースの教師のみ |
DELETE | /courses/tests/{testId} | テストを削除する | 管理者またはコースの教師のみ |
GET | /users/{userId}/test-results | ユーザーのテスト結果を取得する | 管理者または認証済みユーザー |
POST | /courses/tests/{testId}/test-results | ユーザーに関連付けられたテストのテスト結果を作成する | 管理者またはコースの教師のみ |
GET | /courses/tests/{testId}/test-results | テストの複数のテスト結果を取得する | 管理者またはコースの教師のみ |
PUT | /courses/tests/test-results/{testResultId} | テスト結果を更新する(ユーザーとテストに関連付けられている) | 管理者またはテストの採点者のみ |
DELETE | /courses/tests/test-results/{testResultId} | テスト結果を削除する | 管理者またはテストの採点者のみ |
注:
{userId}
のように{}
で囲まれたパラメータを含むパスは、URLで補間される変数を表します。たとえば、www.myapi.com/users/13
では、userId
は13
です。
Hapiでの認可
Hapiのルートには、ハンドラーロジックをより小さく再利用可能な関数に分割できるpre
関数の概念があります。pre
関数はハンドラーの前に呼び出され、レスポンスを引き継ぎ、未認可エラーを返すことができます。これは認可のコンテキストで役立ちます。上記の表で提案されている認可ルールの多くは、複数のルート/エンドポイントで同じになるためです。たとえば、ユーザーが管理者であるかどうかを確認することは、POST /users
ルートとGET /users
ルートの両方で同じになります。これにより、単一のisAdmin
pre関数を再利用し、2つのエンドポイントに割り当てることができます。
ユーザーエンドポイントへの認可の追加
このパートでは、さまざまな認可ルールを実装するためのpre
関数を定義します。まず、リクエストを行うユーザーが管理者であるか、ユーザーが自分のuserId
をリクエストしている場合に認可される必要がある3つの/users/{userId}
エンドポイント(GET
、POST
、およびDELETE
)から始めます。
注: Hapiは、スコープを使用して、ロールベース認証を宣言的に実装する方法も提供しています。ただし、提案されているリソースベースの認可アプローチ(ユーザーの権限がリクエストされた特定のリソースに依存する)には、スコープでは実行できない、よりきめ細かい制御が必要です。そのため、
pre
関数が使用されます。
GET /users/{userId}
ルートで認可ルールを検証するためのpre関数を追加するには、src/plugins/user.ts
に次の関数を宣言します。
次に、src/plugins/user.ts
のルート定義にpreオプションを次のように追加します。
これで、pre関数がgetUserHandler
の前に呼び出され、管理者または自分のuserIdをリクエストするユーザーへのアクセスのみを認可します。
注: 前のパートでは、デフォルトの認証ストラテジーを定義したため、
options.auth
を厳密に定義する必要はありません。ただし、すべてのルートに対して認証要件を明示的に定義することは良い習慣です。
チェックポイント: 認可ロジックが正しく実装されていることを確認するために、テストユーザーとテスト管理者を作成し、/users/{userId}
エンドポイントを呼び出します。
npm run dev
でサーバーを起動します。seed-users
スクリプトを実行して、テストユーザーとテスト管理者を作成します:npm run seed-users
。次のような結果が得られるはずです。
test@prisma.io
としてログインするには、次のようにPOST /login
エンドポイントを呼び出します。
- ログインしたトークンを取得し、curlで
/authenticate
エンドポイントを呼び出します。
- レスポンスは
200
ステータスを持ち、次のようなAuthorization
ヘッダーを含むはずです:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg
/users/1
(番号はチェックポイントの最初のステップで作成されたテストユーザーです)にGETリクエストを送信します。Authorization
ヘッダーには、最後のチェックポイントからのトークンを次のように含めます:curl -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg" localhost:3000/users/1
。リクエストは成功し、ユーザープロフィールが表示されるはずです。- 同じ認可ヘッダーを使用して
/users/2
に別のGETリクエストを送信します:curl -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg" localhost:3000/users/2
。これは403 forbiddenエラーで失敗するはずです。
すべてのステップが成功した場合、isRequestedUserOrAdmin
pre関数は、ユーザーが自分のユーザープロフィールにアクセスすることを正しく認可します。管理者機能をテストするには、3番目のステップから繰り返しますが、メールtest-admin@prisma.io
でテスト管理者としてログインします。管理者は両方のユーザープロフィールを取得できるはずです。
認可pre関数を別のモジュールに移動する
これまでのところ、isRequestedUserOrAdmin
認可pre関数を定義し、それをGET /users/{userId}
ルートに追加しました。さまざまなルートでこれを使用できるようにするには、src/plugins/users.ts
から別のモジュールsrc/auth-helpers.ts
に関数を移動します。このモジュールを使用すると、認可ロジックを1か所に整理し、user-enrollment.ts
のGET /users/{userId}/courses
ルートなど、さまざまなプラグインで定義されたルートに再利用できます。
isRequestedUserOrAdmin
関数をauth-helpers.ts
に移動したら、同じ認可ロジックを持つ次のルートにpre関数として追加します。
モジュール | ルート |
---|---|
src/plugins/users.ts | DELETE /users/{userId} |
src/plugins/users.ts | PUT /users/{userId} |
src/plugins/users-enrollment.ts | GET /users/{userId}/courses |
src/plugins/users-enrollment.ts | POST /users/{userId}/courses |
src/plugins/users-enrollment.ts | DELETE /users/{userId}/courses |
src/plugins/test-results.ts | GET /users/{userId}/test-results |
コース固有のエンドポイントへの認可の追加
教師は、自分が教師であるコースと管理者のコースを更新し、テストを作成できる必要があります。このステップでは、それを検証するための別のpre関数を作成します。
次のpre関数をauth-helpers.ts
に定義します。
pre関数は、validateAPIToken
でフェッチされたteacherOf
配列を使用して、ユーザーがリクエストされたコースの教師であるかどうかを確認します。
isTeacherOfCourseOrAdmin
をpre関数として次のルートに追加します。
モジュール | ルート |
---|---|
src/plugins/courses.ts | PUT /courses/{courseId} |
src/plugins/courses.ts | DELETE /courses/{courseId} |
src/plugins/tests.ts | POST /courses/{courseId}/tests |
次のoptions.pre
を追加して、テーブルからルートを更新します。
これで、2つの異なる認可ルールを実装し、バックエンドの10個の異なるルートにpre関数として追加しました。
テストの更新
REST APIに認証と認可を実装した後、ルートが認証済みユーザーを必要とするようになったため、テストは失敗します。このステップでは、認証を考慮するようにテストを適合させます。
たとえば、GET /users/{userId}
エンドポイントには次のテストがあります。
このテストをnpm run test -- -t="get user returns user"
で今実行すると、テストは失敗します。これは、リクエストがエンドポイントに到達したときに、認証要件を満たしていないためです。Hapiのserver.inject
(サーバーへのHTTPリクエストをシミュレートします)を使用すると、認証済みユーザーに関する情報を含むauth
オブジェクトを追加できます。auth
オブジェクトは、src/plugins/auth.ts
のvalidateAPIToken
関数と同様にcredentialsオブジェクトを設定します。例:
渡されたcredentials
オブジェクトは、src/plugins/auth.ts
で定義されたAuthCredentials
インターフェースと一致します。
注: TypeScriptのインターフェースは、型と非常によく似ていますが、微妙な違いがあります。詳細については、TypeScript Handbookを確認してください。
テストをパスさせるには、テストでPrismaを使用してユーザーを直接作成し、次のようにAuthCredentials
オブジェクトを構築します。
チェックポイント: npm run test -- -t="get user returns user"
を実行して、テストがパスすることを確認します。
現時点では、1つのテストを修正しましたが、他のテストはどうでしょうか? credentialsオブジェクトの作成はほとんどのテストで必要になるため、別のtest-helpers.ts
モジュールに抽象化できます。
次のステップとして、管理者によるGET /users/{userId}
エンドポイントを使用した異なるユーザーアカウントのフェッチを許可する認可ルールを検証するテストを作成します。
まとめと次のステップ
ここまでお疲れ様でした。この記事では、認証と認可の概念から始まり、Prisma、Hapi、およびJWTを使用したメールベースのパスワードレス認証の実装まで、多くの概念を取り上げました。最後に、Hapiのpre関数を使用して認可ルールを実装しました。また、SendGridのAPIを使用してバックエンドにメールを送信する機能を提供するメールプラグインも作成しました。
authプラグインは、認証フローの2つのルートをカプセル化し、jwt
認証スキームを使用して認証ストラテジーを定義しました。認証ストラテジーのvalidate関数では、データベースに対してトークンをチェックし、認可ルールに関連する情報でcredentialsオブジェクトを設定しました。
また、データベースマイグレーションを実行し、Prisma Migrateを使用して、User
テーブルへのn-1リレーションを持つ新しいToken
テーブルを導入しました。
TypeScriptは、型の自動補完と正しい使用の検証(データベーススキーマと同期していることを保証する)に役立ちました。
Prisma Clientを広範囲に使用して、データベース内のデータをフェッチおよび永続化しました。
この記事では、すべてのエンドポイントのサブセットに対する認可について説明しました。次のステップとして、次のことができます。
- 同じ原則に従って、残りのルートに認可を追加します。
- すべてのテストにcredentialオブジェクトを追加します。
JWT_SECRET
環境変数を生成して設定します。SENDGRID_API_KEY
環境変数を設定し、メール機能をテストします。
すべてのエンドポイントに実装された認可ルールと、適合されたテストを含む、完全なソースコードはGitHubで確認できます。
Prismaはリレーショナルデータベースの操作を容易にすることを目指していますが、基盤となるデータベースと認証の原則を理解することは役立ちます。
質問がある場合は、Twitterでお気軽にお問い合わせください。
次回の投稿をお見逃しなく!
Prismaニュースレターにサインアップ