このシリーズの第3部では、REST APIをPrismaでトークンを保存するパスワードレス認証で保護し、認可を実装する方法について見ていきます。
はじめに
このシリーズの目標は、具体的な問題、つまりオンラインコースの成績評価システムを解決することで、最新のバックエンドにおけるさまざまなパターン、問題、アーキテクチャを探求し、実証することです。これは多様なリレーションタイプを特徴とし、現実世界のユースケースを表すのに十分な複雑さを持つため、良い例となります。
ライブストリームの録画は上にあり、この記事と同じ内容をカバーしています。
シリーズの内容
このシリーズでは、バックエンド開発のあらゆる側面におけるデータベースの役割に焦点を当てます。
今日学ぶこと
最初の記事では、問題領域のデータモデルを設計し、Prisma Clientを使用してデータをデータベースに保存するシードスクリプトを作成しました。
このシリーズの2番目の記事では、最初の記事のデータモデルとPrismaスキーマの上にREST APIを構築しました。Hapiを使用してREST APIを構築し、HTTPリクエストを介してリソースに対するCRUD操作を実行できるようにしました。
このシリーズの3番目の記事では、認証と認可の概念、それらの違い、そしてHapiとJSON Web Tokens (JWT)を使用してメールベースのパスワードレス認証と認可を実装し、REST APIを保護する方法について学びます。
具体的には、以下の側面を開発します。
- パスワードレス認証: ユニークなトークンを含むメールを送信することで、ログインとサインアップの機能を追加します。ユーザーは、メールで受け取ったトークンをAPIに送信し、永続的なJWTトークンを受け取ることで認証プロセスを完了し、認証が必要なAPIエンドポイントにアクセスできるようになります。
- 認可: ユーザーがアクセスおよび操作できるリソースを制限するための認可ロジックを追加します。
この記事の終わりまでに、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アプリケーションがパスワードをデータベースに平文で保存することは稀です。代わりに、ハッシュ化と呼ばれる手法を使用して、パスワードのハッシュ値を保存します。これにより、バックエンドはパスワードを知ることなくパスワードを検証できます。
ハッシュ関数は、任意の入力を受け取り、同じ入力に対して常に同じ固定長の文字列/数値を生成する数学的な関数です。ハッシュ関数の力は、パスワードからハッシュを生成できるが、ハッシュからパスワードを生成できないという点にあります。
これにより、実際のパスワードを保存することなく、ユーザーが送信したパスワードを検証できます。パスワードハッシュを保存することで、データベースへの不正アクセスがあった場合でも、ハッシュ化されたパスワードではログインできないため、ユーザーを保護できます。
近年、多くの主要なウェブサイトが侵害されていることを考えると、ウェブセキュリティはますます懸念されるようになっています。この傾向は、多要素認証のようなより安全な認証アプローチを導入することで、セキュリティへのアプローチ方法に影響を与えています。
多要素認証は、ユーザーが2つ以上の証拠(要素とも呼ばれる)を正常に提示した後に認証される認証方法です。たとえば、ATMでお金を引き出す場合、銀行カードの所持とPINコードという2つの認証要素が必要です。
Webアプリケーションではカードの所持を確認することが難しいため、多要素認証は、多くの場合、ユーザー名/パスワードに認証アプリ(スマートフォンにインストールされたアプリや、これらのパスワードを生成する特殊なデバイス)によって生成されたワンタイムトークンを追加することで実装されます。
この記事では、ユーザーエクスペリエンスとセキュリティを向上させる2段階のアプローチである、メールベースのパスワードレス認証を実装します。これは、ログインを試行する際に秘密のトークンをユーザーのメールアカウントに送信することで機能します。ユーザーがメールを開き、トークンをアプリケーションに渡すと、アプリケーションはユーザーを認証し、そのユーザーがメールアカウントの所有者であることを確認できます。
このアプローチは、ユーザーがすでに認証されていると想定できるユーザーのメールサービスに依存しています。ユーザーはパスワードを設定して覚える必要がないため、ユーザーエクスペリエンスが向上します。アプリケーションがパスワード管理の責任から解放されるため、セキュリティが向上します。パスワード管理は攻撃対象となる可能性があります。
認証をユーザーのメールアカウントに外部委託することは、アプリケーションがユーザーのメールアカウントセキュリティの利点と弱点を継承することを意味します。しかし、今日ではほとんどのメールサービスが二要素認証やその他のセキュリティ対策のオプションを提供しています。
それでも、このアプローチはユーザーが弱いパスワードを選択したり、複数のウェブサイトで同じパスワードを再利用したりするのを防ぎます。パスワードを完全に削除することで、これらのユーザーはより安全になります。推測されたり、ブルートフォース攻撃されたり、クラックされたりするパスワードはもはや存在しません。
認証とサインアップ/ログインフロー
メールベースのパスワードレス認証は、2種類のトークンを含む2段階のプロセスです。
認証フローは以下のようになります。
- ユーザーは、認証プロセスを開始するために、ペイロードにメールアドレスを含めてAPIの
/login
エンドポイントを呼び出します。 - メールアドレスが新しい場合、ユーザーはUserテーブルに作成されます。
- メールトークンはバックエンドによって生成され、Tokenテーブルに保存されます。
- メールトークンはユーザーのメールアドレスに送信されます。
- ユーザーは、メールで受け取ったメールトークンとメールアドレスを
/authenticate
エンドポイントに送信します。 - バックエンドはユーザーが送信したメールトークンを検証します。有効でトークンの期限が切れていない場合、JWTトークンが生成され、Tokenテーブルに保存されます。
- JWTトークンは
Authorization
ヘッダーを介してユーザーに返送されます。
2種類のトークンがあります。
- メールトークン: たとえば10分間など、短期間有効な8桁の数値トークンで、ユーザーのメールアドレスに送信されます。このトークンの唯一の目的は、ユーザーがそのメールアドレスと関連付けられていることを検証することであり、成績評価アプリ関連のどのエンドポイントへのアクセスも許可しません。
- 認証トークン: ペイロードに
tokenId
を含むJWTトークンです。このトークンは、APIへのリクエストを行う際にAuthorization
ヘッダーで渡すことで、保護されたエンドポイントにアクセスするために使用できます。このトークンは12時間有効であるという意味で永続的です。
この認証戦略では、単一のエンドポイントがログインと登録の両方を処理します。これは、ログインとサインアップの唯一の違いが、「User」テーブルに行を作成するかどうか(ユーザーがすでに存在する場合)であるため可能です。
JSON Webトークン
JSON Webトークン (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 APIとの簡単な統合のためにSendGridと@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
。バックエンドからトークンがログされているはずです:email token for 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
でサーバーを起動します。- curlを使用して
/login
エンドポイントにPOSTリクエストを実行します:curl --header "Content-Type: application/json" --request POST --data '{"email":"test@test.io"}' localhost:3000/login
。バックエンドからトークンがログされているはずです:email token for 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トークン内のtokenId
を使用してデータベースからユーザー情報を取得することで、JWTトークンによるリクエストを検証するロジックを定義します。
認証戦略を定義するために、auth.ts
に以下を追加します。
authPlugin.register
関数の中に以下を追加します。
最後に、validateAPIToken
関数を追加します。
validateAPIToken
関数は、API_AUTH_STATEGY
(前のステップでデフォルトとして設定したもの)を使用するすべてのルートの前に呼び出されます。
validateAPIToken
関数の目的は、リクエストの続行を許可するかどうかを決定することです。これは、isValid
とcredentials
を含む戻りオブジェクトによって行われます。
isValid
:トークンが正常に検証されたかどうかを決定します。credentials
は、ユーザーに関する情報をリクエストオブジェクトに渡すために使用できます。credentials
に渡されるオブジェクトは、ルートハンドラ内でrequest.auth.credentials
を介してアクセス可能です。
この場合、トークンがデータベースに存在し、有効であり、期限が切れていないと判断します。もしそうであれば、ユーザーが教師であるコース(認可を実装するために使用されます)を取得し、それをtokenId
、userId
、およびisAdmin
とともにクレデンシャルオブジェクトに渡します。
ほとんどのエンドポイントは認証を必要としますが(デフォルトの認証戦略のため)、まだ認可ルールはありません。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に設定されているユーザー)にアクションを認可するのは、ユーザーが「admin」ロールを持っているロールベース認可の一種です。
エンドポイントの認可ルール
提案された認可ルールを実装するために、まず提案された認可ルールを含むエンドポイントのリストを再確認します。
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
事前関数を再利用し、これら2つのエンドポイントに割り当てることができます。
ユーザーエンドポイントへの認可の追加
このパートでは、さまざまな認可ルールを実装するためにpre
関数を定義します。まず、/users/{userId}
の3つのエンドポイント(GET
、POST
、DELETE
)から始めます。これらのエンドポイントは、リクエストを行っているユーザーが管理者である場合、またはユーザーが自身のuserId
を要求している場合に認可されるべきです。
注意: Hapiはスコープを使用してロールベース認証を宣言的に実装する方法も提供しています。しかし、提案されているリソースベースの認可アプローチ(ユーザーの権限が要求された特定のリソースに依存する)では、スコープでは実現できないよりきめ細かい制御が必要なため、
pre
関数が使用されます。
GET /users/{userId}
ルートで認可ルールを検証する事前関数を追加するには、src/plugins/user.ts
に以下の関数を宣言します。
次に、src/plugins/user.ts
のルート定義に、以下のようにpreオプションを追加します。
これで事前関数はgetUserHandler
の前に呼び出され、管理者、または自身のuserIdを要求するユーザーにのみアクセスを許可します。
注意: 前のパートでデフォルトの認証戦略を定義しているため、
options.auth
を定義することは厳密には必須ではありません。しかし、すべてのルートの認証要件を明示的に定義することは良い習慣です。
チェックポイント: 認可ロジックが正しく実装されていることを確認するために、テストユーザーとテスト管理者を作成し、/users/{userId}
エンドポイントを呼び出します。
npm run dev
でサーバーを起動します。- テストユーザーとテスト管理者を作成するために
seed-users
スクリプトを実行します:npm run seed-users
。以下のような結果が表示されるはずです。
- 以下のように
POST /login
エンドポイントを呼び出して、test@prisma.io
としてログインします。
- ログされたトークンを取得し、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
事前関数は、ユーザーが自分のユーザープロファイルにアクセスすることを正しく認可しています。管理者機能をテストするには、3番目のステップから繰り返し、メールアドレスtest-admin@prisma.io
を使用してテスト管理者としてログインします。管理者は両方のユーザープロファイルを取得できるはずです。
認可事前関数を別のモジュールに移動する
ここまでで、isRequestedUserOrAdmin
認可事前関数を定義し、GET /users/{userId}
ルートに追加しました。これを異なるルートで使用するために、src/plugins/users.ts
から別のモジュールであるsrc/auth-helpers.ts
にこの関数を移動します。このモジュールにより、認可ロジックを一箇所に整理し、異なるプラグインで定義されたルート(例:user-enrollment.ts
内のGET /users/{userId}/courses
ルート)で再利用できるようになります。
Once you've moved the isRequestedUserOrAdmin
function into auth-helpers.ts
, add it as a pre-function to the following routes, which have the same authorization logic
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
コース固有のエンドポイントへの認可の追加
教師は、自身が教師であるコースや管理者であるコースの更新やテスト作成ができる必要があります。このステップでは、それを検証するための別の事前関数を作成します。
auth-helpers.ts
に以下の事前関数を定義します。
この事前関数は、validateAPIToken
で取得されるteacherOf
配列を使用して、ユーザーが要求されたコースの教師であるかどうかをチェックします。
isTeacherOfCourseOrAdmin
を以下のルートに事前関数として追加します。
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の異なるルートに事前関数として追加しました。
テストの更新
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
オブジェクトは、src/plugins/auth.ts
で定義されているAuthCredentials
インターフェースと一致します。
注意: TypeScriptのインターフェースは型と非常によく似ていますが、いくつかの微妙な違いがあります。詳細については、TypeScriptハンドブックを参照してください。
テストがパスするには、テスト内でPrismaを使って直接ユーザーを作成し、AuthCredentials
オブジェクトを以下のように構築します。
チェックポイント: npm run test -- -t="get user returns user"
を実行して、テストがパスすることを確認します。
この時点で1つのテストは修正しましたが、他のテストはどうでしょうか?クレデンシャルオブジェクトの作成はほとんどのテストで必要になるため、これを別のtest-helpers.ts
モジュールに抽象化できます。
次のステップとして、管理者がGET /users/{userId}
エンドポイントを使用して異なるユーザーアカウントをフェッチできる認可ルールを検証するテストを記述します。
まとめと次のステップ
ここまでお疲れ様でした。この記事では、認証と認可の概念から始まり、Prisma、Hapi、JWTを使用したメールベースのパスワードレス認証の実装に至るまで、多くの概念をカバーしました。最後に、Hapiの事前関数で認可ルールを実装しました。また、SendGridのAPIでメールを送信する機能をバックエンドに提供するためのメールプラグインも作成しました。
認証プラグインは、認証フローの2つのルートをカプセル化し、jwt
認証スキームを使用して認証戦略を定義しました。認証戦略の検証関数では、トークンをデータベースと照合し、認可ルールに関連する情報でクレデンシャルオブジェクトを埋めました。
また、データベースマイグレーションを実行し、Prisma Migrateを使用してUser
テーブルとのn-1リレーションを持つ新しいToken
テーブルを導入しました。
TypeScriptは、型の正しい使用(データベーススキーマとの同期を保証)を自動補完し、検証するのに役立ちました。
Prisma Clientを幅広く使用して、データベースからデータをフェッチおよび永続化しました。
この記事では、すべてのエンドポイントのサブセットに対する認可について説明しました。次のステップとして、以下を行うことができます。
- 同じ原則に従って、残りのルートに認可を追加します。
- すべてのテストにクレデンシャルオブジェクトを追加します。
JWT_SECRET
環境変数を生成し、設定します。SENDGRID_API_KEY
環境変数を設定し、メール機能をテストします。
すべてのエンドポイントに認可ルールが実装され、テストが適合された完全なソースコードはGitHubで確認できます。
Prismaはリレーショナルデータベースとの作業を容易にすることを目指していますが、基盤となるデータベースと認証の原則を理解することは有用です。
質問があれば、お気軽にTwitterでご連絡ください。
次の投稿をお見逃しなく!
Prismaニュースレターに登録