
API呼び出しの認証には、X.509クライアント証明書からHTTP Basic認証まで、さまざまな選択肢があります。しかし近年では、OAuth 2.0の「アクセストークン」が事実上の標準となっています。これはクライアントからAPIサーバーに渡される認証情報で、通常はHTTPヘッダーに格納されます。
しかし、OAuth 2.0は、相互に関連する標準が複雑に絡み合っています。OAuth 2.0認証フローの発行、提示、検証のプロセスは、多くの場合、複数の関連標準に依存します。本稿執筆時点では、OAuth 2.0標準は8つあります。アクセストークンはその典型例であり、OAuth 2.0のコア仕様(RFC 6749)ではアクセストークンの形式が規定されていません。実際には、次の2つの形式が広く使用されています。
- で定義されているJSON Web Token(JWT)RFC 7519
- 認証済みクライアントの一意の識別子としての役割しか持たない不透明トークン
認証後、クライアントは保護されたリソースにアクセスするため、HTTPリクエストごとにアクセストークンを提示します。アクセストークンが信頼できるIDプロバイダー(IdP)によって発行されたものであり、有効期限が切れていないことを確認するには、アクセストークンの検証が必要です。IdPは発行するJWTに暗号署名を行うため、IdPへの実行時の依存なしにJWTを「オフライン」で検証できます。通常、JWTには有効期限も含まれており、これも確認できます。NGINX Plusの「auth_jwt」モジュールは、JWTのオフライン検証を実行します。
一方、オペークトークンは、発行元のIdPに送り返して検証する必要があります。ただし、この方法には、たとえばグローバルログアウト処理の一環として、以前にログインしたセッションをアクティブなまま残すことなく、IdPがトークンを失効させられるという利点があります。グローバルログアウトでは、JWTについてもIdPによる検証が必要になる場合があります。
このブログでは、NGINXとNGINX PlusがOAuth 2.0のリライングパーティとして機能し、アクセストークンをIdPに送信して検証したうえで、検証に合格したリクエストのみをプロキシする仕組みについて説明します。また、この処理にNGINXとNGINX Plusを使用するさまざまなメリットや、検証レスポンスを短時間キャッシュすることでユーザーエクスペリエンスを向上させる方法についても解説します。さらにNGINX Plusについては、NGINX Plus R18で導入されたJavaScriptモジュールを使用してキーバリューストアを更新し、NGINX Plusインスタンスのクラスター全体にキャッシュを分散する方法も紹介します。
特に記載がない限り、このブログの情報はNGINX Open SourceとNGINX Plusの両方に適用されます。NGINX Plusに関する記述は、NGINX Plusにのみ適用されます。
トークンイントロスペクション
IdPでアクセストークンを検証する標準的な方法は「トークンイントロスペクション」と呼ばれます。RFC 7662(OAuth 2.0 Token Introspection)は現在広くサポートされている標準であり、リライングパーティがIdPにトークンを提示するために使用するJSON/RESTインターフェースと、そのレスポンスの構造を規定しています。主要なIdPベンダーやクラウドプロバイダーの多くがサポートしています。
使用するトークン形式にかかわらず、各バックエンドサービスやアプリケーションで検証を行うと、コードの重複や不要な処理が大量に発生します。さまざまなエラー条件やエッジケースを考慮する必要がありますが、これをバックエンドサービスごとに行うと、実装にばらつきが生じ、その結果、ユーザーエクスペリエンスが予測しにくくなります。各バックエンドサービスが次のエラー条件をどのように処理するかを考えてみましょう。
- アクセストークンがありません
- 極端に大きいアクセストークン
- アクセストークンに無効または予期しない文字が含まれている
- 複数のアクセストークンが提示されている
- バックエンドサービス間のクロックスキュー

NGINXの「auth_request」モジュールを使用したトークンの検証
コードの重複とそれに伴う問題を回避するため、NGINXを使用してバックエンドサービスに代わりアクセストークンを検証できます。これには、次のような利点があります。
- クライアントが有効なトークンを提示した場合にのみ、リクエストがバックエンドサービスに到達します
- 既存のバックエンドサービスは、コードを変更することなく、アクセストークンで保護できます
- IdPへの登録が必要なのはNGINXインスタンスのみで、すべてのアプリを登録する必要はありません
- トークンがない場合や無効な場合を含め、すべてのエラー条件で一貫した動作をします

NGINXを1つ以上のアプリケーションのリバースプロキシとして動作させることで、auth_requestモジュールを使用し、リクエストをバックエンドにプロキシする前にIdPへのAPI呼び出しをトリガーできます。後ほど説明するように、以下のソリューションには根本的な欠陥がありますが、ここではauth_requestモジュールの基本的な動作を紹介します。後のセクションでは、これをさらに発展させます。
コード例を読み込み中…
The auth_requestディレクティブ(5行目)は、API呼び出しを処理するロケーションを指定します。バックエンドへのプロキシ(6行目)は、auth_requestのレスポンスが成功した場合にのみ実行されます。auth_requestロケーションは9行目で定義されています。外部クライアントから直接アクセスされないよう、internalと指定されています。
11~14行目では、トークンイントロスペクションリクエストの形式に準拠するよう、リクエストの各種属性を定義しています。イントロスペクションリクエストで送信されるアクセストークンは、14行目で定義されているボディの構成要素であることに注意してください。ここで、token=$http_apikeyは、クライアントがapikeyリクエストヘッダーでアクセストークンを指定する必要があることを示します。アクセストークンはリクエストの任意の属性で指定できます。その場合は、別のNGINX変数を使用します。
NGINX JavaScript Moduleによる「auth_request」の拡張
前述のとおり、auth_requestモジュールをこのように使用するだけでは、完全な解決策にはなりません。auth_requestモジュールは、HTTPステータスコードを使用して成功かどうかを判定します(2xx = 成功、4xx = 失敗)。しかし、OAuth 2.0トークンイントロスペクションのレスポンスでは、成功または失敗がJSONオブジェクト内にエンコードされ、どちらの場合もHTTPステータスコード200 (OK)が返されます。

必要なのは、IdPのイントロスペクションレスポンスを適切なHTTPステータスコードに変換し、auth_requestモジュールがそのレスポンスを正しく解釈できるようにするJSONパーサーです。
幸い、NGINX JavaScriptモジュール(njs)では、JSONの解析は簡単です。そのため、トークンイントロスペクションリクエストを実行するためのlocationブロックを定義する代わりに、auth_requestモジュールからJavaScript関数を呼び出すようにします。
[編集部注 — この記事は、NGINX JavaScriptモジュールのユースケースを紹介する一連の記事の1つです。全記事の一覧については、「NGINX JavaScriptモジュールのユースケース」をご覧ください。
このセクションのコードは、js_importディレクティブを使用するように更新されています。このディレクティブは、js_includeディレクティブに代わるもので、NGINX Plus R23以降で使用されます。詳細については、NGINX JavaScript moduleのリファレンスドキュメントを参照してください。Example Configurationセクションでは、NGINX設定ファイルとJavaScriptファイルの正しい構文を確認できます。]
注: このソリューションを使用するには、JavaScriptモジュールを動的モジュールとしてload_moduleディレクティブでnginx.confに読み込む必要があります。手順については、NGINX Plus管理ガイドを参照してください。
コード例を読み込み中…
13行目の「js_content」ディレクティブでは、JavaScript関数「introspectAccessToken」を「auth_request」ハンドラーとして指定しています。このハンドラー関数は「oauth2.js」で定義されています。
コード例を読み込み中…
ここで、introspectAccessToken関数は、以下の構成スニペットで定義されている別のロケーション(/oauth2_send_request)に対してHTTPサブリクエスト(2行目)を送信します。次に、JavaScriptコードはレスポンスを解析し(5行目)、auth_requestモジュールに対して、activeフィールドの値に基づく適切なステータスコードを返します。有効(アクティブ)なトークンの場合はHTTP 204 (No Content)(成功)が返され、無効なトークンの場合はHTTP 403 (Forbidden)が返されます。エラーが発生した場合はHTTP 401 (Unauthorized)が返されるため、エラーと無効なトークンを区別できます。
注: このコードは概念実証のみを目的として提供されており、本番環境での使用に適した品質ではありません。包括的なエラー処理とログ記録を備えた完全なソリューションについては、以下を参照してください。
2行目で定義されているサブリクエストのターゲットロケーションは、元の「auth_request」設定とよく似ています。
コード例を読み込み中…
トークンイントロスペクションリクエストを構成するための設定は、すべて/_oauth2_send_requestロケーション内に含まれています。通常、必要な設定項目は、認証(19行目)、アクセストークン自体(21行目)、トークンイントロスペクションエンドポイントのURL(22行目)のみです。IdPがこのNGINXインスタンスからのトークンイントロスペクションリクエストを受け入れるには、認証が必要です。「OAuth 2.0 Token Introspection」仕様では認証が必須とされていますが、その方式は規定されていません。この例では、Authorizationヘッダーでベアラートークンを使用します。
この設定を適用すると、NGINXは受信したリクエストをJavaScriptモジュールに渡し、このモジュールがIdPに対してトークンイントロスペクションリクエストを行います。IdPからのレスポンスが検査され、activeフィールドがtrueの場合、認証は成功したとみなされます。このソリューションにより、NGINXでOAuth 2.0トークンイントロスペクションをコンパクトかつ効率的に実行でき、他の認証APIにも容易に適用できます。
しかし、これで完了ではありません。一般に、トークンイントロスペクションの最大の課題は、すべてのHTTPリクエストにレイテンシーが加わることです。対象のIdPがホステッドソリューションやクラウドプロバイダーである場合、これは重大な問題になり得ます。NGINXとNGINX Plusでは、イントロスペクションのレスポンスをキャッシュすることで、この欠点を軽減できます。
最適化1:NGINXによるキャッシュ
OAuth 2.0トークンイントロスペクションは、IdPによってJSON/RESTエンドポイントで提供されるため、標準レスポンスはHTTPステータス200のJSON本文です。このレスポンスをアクセストークンに関連付けることで、効率的にキャッシュできるようになります。

NGINXでは、アクセストークンごとにイントロスペクションレスポンスのコピーをキャッシュするよう設定できます。これにより、同じアクセストークンが次回提示された際、IdPへのAPI呼び出しを行う代わりに、キャッシュされたイントロスペクションレスポンスを返します。その結果、後続リクエストの全体的なレイテンシーが大幅に改善されます。キャッシュされたレスポンスの使用期間を制御することで、有効期限切れまたは直近で失効したアクセストークンを受け入れるリスクを軽減できます。たとえば、APIクライアントが通常、短時間に複数のAPI呼び出しを連続して行う場合、キャッシュの有効期間を10秒に設定するだけでも、ユーザーエクスペリエンスを目に見えて改善できる可能性があります。
キャッシュを有効にするには、その保存先として、キャッシュ(イントロスペクション応答)用のディスク上のディレクトリと、キー(アクセストークン)用の共有メモリゾーンを指定します。
コード例を読み込み中…
The proxy_cache_pathディレクティブは、必要なストレージを割り当てます。イントロスペクションレスポンス用の/var/cache/nginx/oauthと、キー用のtoken_responsesというメモリゾーンです。このディレクティブはhttpコンテキストで設定されるため、serverブロックとlocationブロックの外部に記述します。その後、トークンイントロスペクションレスポンスを処理するlocationブロック内でキャッシュを有効にします。
コード例を読み込み中…
このlocationでは、proxy_cacheディレクティブ(26行目)によってキャッシュが有効になります。デフォルトでは、NGINXはURIに基づいてキャッシュしますが、ここではapikeyリクエストヘッダーで提示されたアクセストークンに基づいてレスポンスをキャッシュします(27行目)。
28行目では、proxy_cache_lockディレクティブを使用して、同じキャッシュキーを持つ複数のリクエストが同時に到着した場合、最初のリクエストによってキャッシュが生成されるまで待ってから、他のリクエストに応答するようNGINXに指示します。proxy_cache_validディレクティブ(29行目)は、イントロスペクションレスポンスをキャッシュする期間をNGINXに指定します。このディレクティブがない場合、NGINXはIdPから送信されるCache-Controlヘッダーに基づいてキャッシュ期間を決定します。ただし、これらのヘッダーは必ずしも信頼できるとは限りません。そのため、レスポンスのキャッシュ方法に影響するヘッダーを無視するようNGINXに指示しています(30行目)。
キャッシュが有効になったため、アクセストークンを提示するクライアントで発生する遅延は、10秒に1回のトークンイントロスペクションリクエストによるものだけです。
最適化2:NGINX Plusによる分散キャッシュ
コンテンツキャッシュとトークンイントロスペクションを組み合わせることで、セキュリティへの影響を最小限に抑えながら、アプリケーション全体のパフォーマンスを大幅に向上できます。ただし、複数のデータセンターやクラウドプラットフォーム、アクティブ/アクティブクラスターなどにNGINXを分散配置している場合、キャッシュされたトークンイントロスペクションのレスポンスを利用できるのは、イントロスペクションリクエストを実行したNGINXインスタンスのみです。
NGINX Plusでは、インメモリのキーバリューストアであるkeyvalモジュールを使用して、トークンイントロスペクションのレスポンスをキャッシュできます。さらに、zone_syncモジュールを使用すると、NGINX Plusインスタンスのクラスター全体でこれらのレスポンスを同期することもできます。これにより、どのNGINX Plusインスタンスがトークンイントロスペクションリクエストを実行した場合でも、クラスター内のすべてのNGINX Plusインスタンスでレスポンスを利用できます。
注: ランタイム状態を共有するためのzone_syncモジュールの設定については、このブログでは扱いません。NGINX Plusクラスターでの状態共有の詳細については、NGINX Plus管理者ガイドをご覧ください。
In NGINX Plus R18以降では、keyvalディレクティブで宣言された変数を変更することで、キーバリューストアを更新できます。JavaScriptモジュールはすべてのNGINX変数にアクセスできるため、レスポンスの処理中にイントロスペクションレスポンスをキーバリューストアへ格納できます。
NGINXファイルシステムキャッシュと同様に、キー(アクセストークン)と値(イントロスペクションレスポンス)を格納するストレージを指定することで、キー・バリューストアが有効になります。この場合、ストレージにはメモリゾーンを指定します。
コード例を読み込み中…
なお、timeoutパラメーターをkeyval_zoneディレクティブに指定し、キャッシュされたレスポンスの有効期間をauth_request_cache.confの29行目と同じ10秒に設定しています。これにより、有効期限が切れると、NGINX Plusクラスターの各メンバーが個別にレスポンスを削除します。2行目では、各エントリのキーと値のペアを指定しています。キーはapikeyリクエストヘッダーで提供されるアクセストークン、値は$token_data変数によって評価されたイントロスペクションレスポンスです。
これにより、apikeyリクエストヘッダーを含む各リクエストでは、以前のトークンイントロスペクションレスポンスが存在する場合、$token_data変数にそのレスポンスが格納されます。そのため、トークンイントロスペクションレスポンスがすでに存在するかどうかを確認するようにJavaScriptコードを更新します。
コード例を読み込み中…
2行目では、このアクセストークンに対応するエントリがキー・バリューストアにすでに存在するかどうかを確認します。イントロスペクションレスポンスを取得する経路は2つ(キー・バリューストアから取得する経路と、イントロスペクションレスポンスから取得する経路)あるため、検証ロジックを次の独立した関数に移します。tokenResult:
コード例を読み込み中…
これで、各トークンイントロスペクションのレスポンスがキーバリューストアに保存され、NGINX Plusクラスターの他のすべてのメンバーと同期されます。次の例では、有効なアクセストークンを使用した単純なHTTPリクエストに続いて、NGINX Plus APIにクエリを実行し、キーバリューストアの内容を表示します。
$ curl -IH "apikey: tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA" http://localhost/HTTP/1.1 200 OK
Date: Wed, 24 Apr 2019 17:41:34 GMT
Content-Type: application/json
Content-Length: 612
$ curl http://localhost/api/4/http/keyvals/access_tokens
{"tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA":"{\"active\":true}"}キー・バリューストア自体がJSON形式を使用するため、トークンイントロスペクションのレスポンス内の引用符は自動的にエスケープされる点に注意してください。
最適化3:イントロスペクションレスポンスからの属性の抽出
OAuth 2.0トークンイントロスペクションの便利な機能の1つは、レスポンスにトークンのアクティブ状態だけでなく、トークンに関する情報も含められることです。この情報には、トークンの有効期限や、関連付けられたユーザーの属性(ユーザー名、メールアドレスなど)が含まれます。

この追加情報は非常に有用です。ログに記録したり、きめ細かなアクセス制御ポリシーの実装に使用したり、バックエンドアプリケーションに提供したりできます。成功を示す(HTTP auth_request モジュールに、追加のレスポンスヘッダーとして送信することで、これらの各属性をエクスポートできます(HTTP 204)レスポンス)。
コード例を読み込み中…
イントロスペクションレスポンスの各属性を順に処理し(23行目)、レスポンスヘッダーとしてauth_requestモジュールに返します。標準のレスポンスヘッダーとの競合を避けるため、各ヘッダー名にはToken-というプレフィックスを付けます(26行目)。これらのレスポンスヘッダーはNGINX変数に変換し、通常の設定の一部として使用できるようになります。
コード例を読み込み中…
この例では、username属性を新しい変数$usernameに変換します(11行目)。auth_request_setディレクティブを使用すると、トークンイントロスペクションレスポンスのコンテキストを現在のリクエストのコンテキストにエクスポートできます。JavaScriptコードによって追加された各属性のレスポンスヘッダーは、$sent_http_token_attributeとして参照できます。次に、12行目で$usernameの値を、バックエンドにプロキシされるリクエストヘッダーとして追加します。トークンイントロスペクションレスポンスで返されるどの属性にも、この設定を適用できます。

本番環境の構成
上記のコードと設定例は実際に動作し、概念実証(PoC)テストや特定のユースケースに合わせたカスタマイズに適しています。本番環境で使用する場合は、エラー処理やロギングを追加し、柔軟に設定できるようにすることを強く推奨します。NGINXおよびNGINX Plus向けの、より堅牢で詳細な実装については、F5のGitHubリポジトリをご覧ください。
まとめ
このブログでは、NGINXの「auth_request」モジュールとJavaScriptモジュールを組み合わせ、クライアントリクエストに対してOAuth 2.0トークンイントロスペクションを実行する方法を紹介しました。さらに、このソリューションにキャッシュ機能を追加し、イントロスペクションレスポンスから属性を抽出してNGINXの設定で使用できるようにしました。
また、NGINX Plusのキー・バリューストアを、NGINX Plusインスタンスのクラスター全体にわたる本番環境への導入に適した、イントロスペクション応答用の分散キャッシュとして使用する方法についても説明しました。
NGINX PlusによるOAuth 2.0トークンイントロスペクションをぜひお試しください。今すぐ30日間の無料トライアルを開始するか、ユースケースについてご相談ください。
著者について

関連するブログ記事
チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する
OpenTelemetryを使用してNGINXサーバーの状態と健全性をより明確に把握することで、問題のトラブルシューティングを行い、Webアプリケーションのパフォーマンスを向上できます。
AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正
Akita SoftwareのAPIオブザーバビリティ向け新しいNGINX Plus Certified Moduleを活用して、APIエンドポイントの問題をより迅速に特定し、修正できます。
JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法
Red HatのシニアプリンシパルデベロッパーアドボケイトであるDaniel Oh氏が、Javaプロジェクトをゼロから構築するためのひな形を作成し、Kubernetesネイティブアプリケーションとしてビルドする方法を解説します。
F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング
NGINXaaS for Azureは、NGINX PlusをAzureネイティブのSaaSとして利用できるサービスです。NGINXの設定を容易にリフト&シフトできるため、クラウド導入とモダンアプリの展開を加速できます。

SELinuxでNGINXおよびNGINX Plusを使用する
Red Hat Enterprise Linux(RHEL)および関連ディストリビューションでSecurity-Enhanced Linux(SELinux)を有効にすると、デフォルト設定によりNGINXとNGINX Plusの一部の操作が制限されます。この記事では、すべての機能を利用できるようにSELinuxの設定を変更する方法について説明します。
NGINX PlusによるAWS Auto Scalingグループの負荷分散
新しいnginx-asg-sync統合ソフトウェアにより、NGINX PlusはAWS Auto Scalingグループの負荷を直接分散できるため、クラウドでの負荷分散がさらに柔軟になります