この記事では、Azure API Management を使用して、ブラウザー セッションにトークンを格納しない JavaScript シングルページ アプリケーションのステートレス アーキテクチャを実装する方法について説明します。 このアプローチは、クロスサイト スクリプティング (XSS) 攻撃からアクセス トークンを保護し、悪意のあるコードがブラウザーで実行されるのを防ぐのに役立ちます。
このアーキテクチャでは、 API Management を使用して次のタスクを実行します。
- Microsoft Entra ID から OAuth2 アクセス トークンを取得する バックエンド for Frontends パターン を実装する
- Advanced Encryption Standard AES を使用してアクセス トークンを暗号化および復号化する
-
HttpOnlyCookie にトークンを格納する - 承認を必要とするすべての API 呼び出しをプロキシする
バックエンドはトークンの取得を処理するため、Microsoft Authentication Library for JavaScript (MSAL.js) などの他のコードやライブラリは、シングルページ アプリケーションでは必要ありません。 この設計を使用する場合、トークンはブラウザー セッションまたはローカル ストレージに格納されません。 アクセス トークンを暗号化して HttpOnly Cookie に格納すると、 XSS 攻撃からアクセス トークンを保護できます。 API ドメインにスコープを設定し、SameSite を Strict に設定すると、プロキシされたすべての API ファースト パーティ要求で Cookie が自動的に送信されます。
アーキテクチャ
このアーキテクチャの Visio ファイルをダウンロードします。
Workflow
ユーザーがシングルページ アプリケーション サインイン を選択します。
シングルページ アプリケーションは、Microsoft Entra 承認エンドポイントへのリダイレクトを介して承認コード フローを呼び出します。
ユーザーは自分自身を認証します。
承認コードを含む承認コード フロー応答は、API Management コールバック エンドポイントにリダイレクトされます。
API Management ポリシーは、Microsoft Entra トークン エンドポイントを呼び出すことによって、承認コードをアクセス トークンと交換します。
API Management ポリシーは、アプリケーションにリダイレクトし、暗号化されたアクセス トークンを
HttpOnlyCookie に配置します。ユーザーは、API Management プロキシエンドポイントを介してアプリケーションから外部 API 呼び出しを呼び出します。
API Management ポリシーは、API 要求を受け取り、Cookie を復号化し、ダウンストリーム API 呼び出しを行ってアクセス トークンを
Authorizationヘッダーとして追加します。
コンポーネント
Microsoft Entra ID は、Azure ワークロード全体で ID サービス、シングル サインオン、多要素認証を提供するクラウドベースの ID およびアクセス管理サービスです。 このアーキテクチャでは、Microsoft Entra ID がユーザーを認証し、アクセス トークンを発行します。
API Management は、すべての環境にわたる API 用のハイブリッド マルチクラウド管理プラットフォームです。 API Management は、既存のバックエンド サービス用に一貫性のある最新の API ゲートウェイを作成します。 このアーキテクチャでは、API Management を使用して、Microsoft Entra ID とプロキシ API 呼び出しからアクセス トークンを取得するバックエンド for フロントエンド パターンを実装します。
Azure Storage での静的 Web サイト ホスティングでは、Azure Blob Storage が使用されます。 コンテンツをレンダリングするために Web サーバーが必要ない場合に、静的な Web サイト ホスティングのサポートを提供するのに最適です。 このアーキテクチャでは、静的な Web サイト ホスティングを使用して、シングルページ アプリケーションをホストします。 シングルページ アプリケーションは、ブラウザーで実行され、API Management ゲートウェイを呼び出してバックエンド API にアクセスする JavaScript アプリケーションです。
シナリオの詳細
シングルページ アプリケーションは JavaScript で記述され、クライアント側ブラウザーのコンテキストで実行されます。 この実装では、ユーザーはブラウザーで実行されている任意のコードにアクセスできます。 ブラウザーまたは XSS 攻撃で実行される悪意のあるコードもデータにアクセスする可能性があります。 ブラウザー セッションまたはローカル ストレージに格納されているデータにアクセスできます。 その結果、アクセス トークンなどの機密データを使用してユーザーを偽装できます。
この記事で説明するアーキテクチャでは、トークンの取得とストレージをバックエンドに移動し、暗号化された HttpOnly Cookie を使用してアクセス トークンを格納することで、アプリケーションのセキュリティを強化します。 アクセス トークンは、ブラウザー セッションまたはローカル ストレージに格納する必要はありません。また、ブラウザーで実行される悪意のあるコードはアクセスできません。
このアーキテクチャでは、API Management ポリシーはアクセス トークンの取得と Cookie の暗号化と暗号化解除を処理します。 ポリシー は、API の要求または応答で順番に実行され、XML 要素と C# スクリプトで構成されるステートメントのコレクションです。
トークンを HttpOnly Cookie に格納すると、XSS 攻撃からトークンを保護し、JavaScript からアクセスできないようにするのに役立ちます。 Cookie を API ドメインにスコープし、SameSite を Strict に設定すると、プロキシされたすべての API ファースト パーティ要求で Cookie が自動的に送信されます。 この設計により、バックエンドがシングルページ アプリケーションから行うすべての API 呼び出しの Authorization ヘッダーにアクセス トークンを自動的に追加できます。
このアーキテクチャでは SameSite=Strict Cookie が使用されるため、API Management ゲートウェイのドメインはシングルページ アプリケーションのドメインと同じである必要があります。 API 要求が同じドメイン内のサイトから送信された場合にのみ、Cookie が API Management ゲートウェイに送信されます。 ドメインが異なる場合、Cookie は API 要求に追加されず、プロキシされた API 要求は認証されません。
API Management インスタンスと静的 Web サイト ホスティングにカスタム ドメインを使用せずに、このアーキテクチャを構成できます。 このシナリオでは、cookie 設定に SameSite=None を使用する必要があります。 この設定により、API Management ゲートウェイの任意のインスタンスに対するすべての要求に Cookie が追加されるため、実装の安全性が低下します。 詳細については、「SameSite Cookie する」を参照してください。
Azure リソースにカスタム ドメインを使用する方法の詳細については、「 カスタム ドメインを Blob Storage エンドポイントにマップする 」および 「API Management インスタンスのカスタム ドメイン名を構成する」を参照してください。 カスタム ドメインのドメイン ネーム システム (DNS) レコードを構成する方法の詳細については、 Azure portal での DNS ゾーンの管理に関するページを参照してください。
認証フロー
このプロセスでは、 OAuth2 承認コード フローが使用されます。 シングルページ アプリケーションが API にアクセスできるようにするアクセス トークンを取得するには、まずユーザーが自分自身を認証する必要があります。 ユーザーを Microsoft Entra 承認エンドポイントにリダイレクトすることで、認証フローを呼び出します。 Microsoft Entra ID でリダイレクト用のユニフォーム リソース アイデンティファイア (URI) を構成する必要があります。 このリダイレクト URI は、API Management コールバック エンドポイントである必要があります。 ユーザーは、Microsoft Entra ID を使用して自分自身を認証するように求められ、承認コードを使用して API Management コールバック エンドポイントにリダイレクトされます。 その後、API Management ポリシーは、Microsoft Entra トークン エンドポイントを呼び出して、アクセス トークンの承認コードを交換します。 次の図は、このフローのイベントのシーケンスを示しています。
フローには、次の手順が含まれています。
シングルページ アプリケーションが API にアクセスできるようにするためのアクセス トークンを取得するには、まずユーザーが自身を認証する必要があります。 ユーザーは、Microsoft ID プラットフォーム承認エンドポイントにリダイレクトするボタンを選択してフローを呼び出します。
redirect_uriは、API Management ゲートウェイの/auth/callbackAPI エンドポイントに設定されます。ユーザーは自分自身を認証するように求められます。 認証が成功すると、Microsoft ID プラットフォームはリダイレクトで応答します。
ブラウザーは、API Management コールバック エンドポイントである
redirect_uriにリダイレクトされます。 承認コードはコールバック エンドポイントに渡されます。コールバック エンドポイントの受信ポリシーが呼び出されます。 ポリシーは、Microsoft Entra トークン エンドポイントに要求を発行することで、承認コードをアクセス トークンと交換します。 クライアント ID、クライアント シークレット、承認コードなど、必要な情報が渡されます。
<send-request ignore-error="false" timeout="20" response-variable-name="response" mode="new"> <set-url>https://login.microsoftonline.com/{{tenant-id}}/oauth2/v2.0/token</set-url> <set-method>POST</set-method> <set-header name="Content-Type" exists-action="override"> <value>application/x-www-form-urlencoded</value> </set-header> <set-body>@($"grant_type=authorization_code&code={context.Request.OriginalUrl.Query.GetValueOrDefault("code")}&client_id={{client-id}}&client_secret={{client-secret}}&redirect_uri=https://{context.Request.OriginalUrl.Host}/auth/callback")</set-body> </send-request>アクセス トークンが返され、
tokenという名前の変数に格納されます。<set-variable name="token" value="@((context.Variables.GetValueOrDefault<IResponse>("response")).Body.As<JObject>())" />アクセス トークンは AES 暗号化で暗号化され、
cookieという名前の変数に格納されます。<set-variable name="cookie" value="@{ var rng = new RNGCryptoServiceProvider(); var iv = new byte[16]; rng.GetBytes(iv); byte[] tokenBytes = Encoding.UTF8.GetBytes((string)(context.Variables.GetValueOrDefault<JObject>("token"))["access_token"]); byte[] encryptedToken = tokenBytes.Encrypt("Aes", Convert.FromBase64String("{{enc-key}}"), iv); byte[] combinedContent = new byte[iv.Length + encryptedToken.Length]; Array.Copy(iv, 0, combinedContent, 0, iv.Length); Array.Copy(encryptedToken, 0, combinedContent, iv.Length, encryptedToken.Length); return System.Net.WebUtility.UrlEncode(Convert.ToBase64String(combinedContent)); }" />シングルページ アプリケーションにリダイレクトするために、コールバック エンドポイントの送信ポリシーが呼び出されます。
HttpOnlyに設定され、API Management ゲートウェイのドメインにスコープが設定SameSite、StrictCookie に暗号化されたアクセス トークンが設定されます。 明示的な有効期限が設定されていないため、Cookie はセッション Cookie として作成され、ブラウザーが閉じられると有効期限が切れます。<return-response> <set-status code="302" reason="Temporary Redirect" /> <set-header name="Set-Cookie" exists-action="override"> <value>@($"{{cookie-name}}={context.Variables.GetValueOrDefault<string>("cookie")}; Secure; SameSite=Strict; Path=/; Domain={{cookie-domain}}; HttpOnly")</value> </set-header> <set-header name="Location" exists-action="override"> <value>{{return-uri}}</value> </set-header> </return-response>
API 呼び出しフロー
シングルページ アプリケーションにアクセス トークンがある場合は、トークンを使用してダウンストリーム API を呼び出すことができます。 Cookie のスコープはシングルページ アプリケーションのドメインであり、 SameSite=Strict 属性を使用して構成されるため、要求に自動的に追加されます。 その後、アクセス トークンを復号化して、ダウンストリーム API の呼び出しに使用できます。 次の図は、このフローのイベントのシーケンスを示しています。
フローには、次の手順が含まれています。
ユーザーがシングルページ アプリケーションでボタンを選択して、ダウンストリーム API を呼び出します。 このアクションは、API Management ゲートウェイの
/graph/meAPI エンドポイントを呼び出す JavaScript 関数を呼び出します。Cookie のスコープはシングルページ アプリケーションのドメインであり、
SameSiteStrictに設定されているため、ブラウザーは API に要求を送信するときに Cookie を自動的に追加します。API Management ゲートウェイが要求を受信すると、
/graph/meエンドポイントの受信ポリシーが呼び出されます。 このポリシーは、Cookie からアクセス トークンを復号化し、access_tokenという名前の変数に格納します。<set-variable name="access_token" value="@{ try { string cookie = context.Request.Headers .GetValueOrDefault("Cookie")? .Split(';') .ToList()? .Where(p => p.Contains("{{cookie-name}}")) .FirstOrDefault() .Replace("{{cookie-name}}=", ""); byte[] encryptedBytes = Convert.FromBase64String(System.Net.WebUtility.UrlDecode(cookie)); byte[] iv = new byte[16]; byte[] tokenBytes = new byte[encryptedBytes.Length - 16]; Array.Copy(encryptedBytes, 0, iv, 0, 16); Array.Copy(encryptedBytes, 16, tokenBytes, 0, encryptedBytes.Length - 16); byte[] decryptedBytes = tokenBytes.Decrypt("Aes", Convert.FromBase64String("{{enc-key}}"), iv); char[] convertedBytesToChar = Encoding.UTF8.GetString(decryptedBytes).ToCharArray(); return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(convertedBytesToChar)); } catch (Exception ex) { return null; } }" />アクセス トークンは、
Authorizationヘッダーとしてダウンストリーム API への要求に追加されます。<choose> <when condition="@(!string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("access_token")))"> <set-header name="Authorization" exists-action="override"> <value>@($"Bearer {context.Variables.GetValueOrDefault<string>("access_token")}")</value> </set-header> </when> </choose>要求は、
Authorizationヘッダーへのアクセス トークンを含め、ダウンストリーム API にプロキシされます。ダウンストリーム API からの応答は、シングルページ アプリケーションに直接返されます。
このシナリオのデプロイ
この記事で説明するポリシーの完全な例、OpenAPI 仕様、および完全なデプロイ ガイドについては、この GitHub リポジトリを参照してください。
[拡張]
このソリューションは運用環境に対応していません。 これは、この記事で説明するサービスを使用してできることのデモンストレーションを目的としています。 運用環境でソリューションを使用する前に、次の要因を考慮してください。
この例では、アクセス トークンの有効期限や、更新トークンまたは ID トークンの使用は実装しません。
サンプル内の Cookie の内容は、AES 暗号化を使用して暗号化されます。 キーは、API Management インスタンスの 名前付き値 ペインにシークレットとして格納されます。 この名前付き値をより適切に保護するために、Azure Key Vault に格納されているシークレットへの参照使用できます。 キー管理 ポリシーの一部として、暗号化キーを定期的にローテーションする必要があります。
この例では、単一のダウンストリーム API への呼び出しのみをプロキシするため、必要なアクセス トークンは 1 つだけです。 このシナリオでは、ステートレス アプローチを使用できます。 ただし、HTTP Cookie のサイズ制限のため、複数のダウンストリーム API への呼び出しをプロキシする必要がある場合は、ステートフルなアプローチが必要です。
この方法では、1 つのアクセス トークンを使用する代わりに、アクセス トークンをキャッシュに格納し、呼び出されている API と Cookie で提供されるキーに基づいて取得します。 この方法は、API Management キャッシュ または外部の Redis キャッシュを使用して実装できます。
この例では、GET 要求を介してのみデータを取得する方法を示しているため、 クロスサイト リクエスト フォージェリ (CSRF) 攻撃に対する保護は提供されません。 POST、PUT、PATCH、DELETE などの他の HTTP メソッドを使用する場合は、この保護が必要です。
貢献者達
Microsoft では、この記事を保持しています。 次の共同作成者がこの記事を書きました。
主要著者:
- イラ・レイニー |プリンシパル ソフトウェア エンジニア
公開されていない LinkedIn プロフィールを見るには、LinkedIn にサインインしてください。
次のステップ
- 実装とデプロイの例に関するガイド
- API Management のポリシー
- API Management ポリシーを設定または編集する
- API Management ポリシーで名前付き値を使用する
- Microsoft Entra ID を使用した OAuth 2.0 認証の
- Storage で静的 Web サイトをホストする