Azure API ManagementでOn-Behalf-Of flow (OBO flow) を実現したい

このエントリは2025/06/27現在の情報に基づいています。将来の機能追加や変更に伴い、記載事項からの乖離が発生する可能性があります。

問い合わせ

Azure API Management (APIM) をお使いの主から以下のような問い合わせが届いた。

現在APIMを使ってREST APIを公開している。公開している各APIでは認証・認可を構成しているが、On-Behalf-Of flow (OBO flow) を使ってバックエンドサービスの認可を実現したいのだが、以下についてコメントがほしい。

  1. APIMで実現する方法はあるか?
  2. Credential ManagerはOBO Flowを実現できるか?
  3. 管理性・セキュリティ・パフォーマンスを考慮した推奨構成はあるか?

主の懸念は以下のあたり。「せやな」と首肯するばかり。

  • APIMがOAuth 2.0 authorization code grantを使ってもよいが、これだと、APIMはバックエンドサービスが必要とする権限を全て包含した形でユーザー同意を得るため、クライアントは過大な権限を持つアクセストークンを取得してしまう危険性がある。これを避けるため、On-Behalf-Of (OBO) flowにより、元のアクセストークンから権限を縮小したアクセストークンを取得したい。
  • ただ、APIMにはこれを実現するための直接的なポリシーがないように見えるので、そもそも実現可能なのか知りたい。

なお、OBO flowについては以下のドキュメントを参照。

Microsoft ID プラットフォームと OAuth2.0 On-Behalf-Of フロー / Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow
https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow

そもそも実現可能か?

確かに、APIM自体はOAuth 2.0の一部拡張をネイティブに自動実行する機能を持たない。しかしながら、ポリシー機能を活用すれば、 APIMを、「中間層のWeb API」、つまり下図のWeb API AとしてOBO flowに参加させることができる。

すなわち、

  • APIMが受け取ったユーザーのアクセストークンを使ってEntra IDのトークンエンドポイントに対しHTTP要求を発行し、新たなアクセストークンを取得
  • バックエンドに付け替える

という処理を、ポリシーで実装すればよい。

具体的な実装シナリオは以下。

  • APIMのInboundセクションでsend-requestポリシーを用い、受信したJWTをOBO flow用のassertionとしてEntra IDにPOST送信することで、バックエンドAPI用のaccess tokenを取得し直す。
  • 取得後はAPIM内でAuthorizationヘッダーを書き換えてからバックエンドに転送することで、バックエンドサービスはユーザーの代理トークンを使ったリクエストを受け取る。

Credential ManagerやバックエンドのAuthorization credentialsでは、OBO Flowを実現できるか?

Credential Managerは、Azure API Managementの機能で、バックエンドへの認証に必要な資格情報を安全に保管・管理するための仕組み。

API 資格情報と資格情報マネージャーについて / About API credentials and credential manager
https://learn.microsoft.com/azure/api-management/credentials-overview

Authorization credentialsはBackend構成時に認証情報を追加できるしくみ。

バックエンドを作成する / Create a backend
https://learn.microsoft.com/azure/api-management/backends?tabs=bicep#create-a-backend

現在のCredential Manager機能が想定する主なシナリオは、APIMが自らクライアントとなってバックエンドのトークンを取得するケース(クライアント資格情報フロー / client credentials flowや認可コードフロー / authorization code flowの代行)で、Credential Managerによるトークン管理は、APIMがIdPからトークンを取得するところまでを想定している。換言すると、既に受け取ったユーザーのアクセストークンを使って別トークンを交換取得するという 特殊なシナリオには対応する仕組みが用意されていない。それゆえ、現時点でAPIMのCredential Manager機能はOBO flowを直接にはサポートしていない。

Authorization credentialsの場合も同様で、OBO flowを直接にはサポートしていない。

そのため、OBO flowを実現するためには、APIMポリシーによるカスタム処理が必要である。

管理性・セキュリティ・パフォーマンスを考慮した推奨構成はあるか?

いわゆる「ベストプラクティス教えて」ってやつなのだが、結論から言うと、

というもの。以下でそれぞれの方式を説明していく。

実装方式

以下の2方式がある。

  1. Client ID/Client Secretを使って実装する(いわゆる「ふつうの構成」方法)
  2. managed identityを使う方法(先ほど推奨方式と記載した方法)

1. ふつうに実装する

①APIMをEntra IDへアプリとして登録

中間層として機能するAPIMを表すアプリケーションをEntra IDに登録し、Client IDおよびClient Secretを発行しておく。なおこのアプリは、後述のユーザートークン (トークンA) の受信者 (aud) ユーザー視点では「このアプリがあなたの代理で他のAPIにアクセス」という同意画面が表示される対象である。

②バックエンドAPIごとのアプリ登録

統合する各バックエンドサービス(API B、C、…)に対し、それぞれEntra ID上でリソース用アプリとして登録しておく。各バックエンドアプリには、そのAPIが提供する機能に応じたスコープ(委任許可)を定義する。細分化された権限を設定しておくことで、後段のOBO flowでのトークン取得時に必要最小のスコープを指定できる。

③アプリ間の許可とユーザー同意の設定

OBOフローで必要なユーザーの同意を事前に確保する。

  • APIMアプリにバックエンドAPIへのアクセス許可を付与
    • Entra IDの「APIのアクセス許可」設定で、APIMゲートウェイ用アプリに対し、バックエンドAPI用アプリが公開するスコープを委任された権限として追加

【例】「APIMアプリがAPI_01のRead.All権限をOn behalf Ofで呼び出すことを許可」などの形で、必要な組み合わせの許可を構成

  • ユーザーの同意
    • ユーザーが最初にクライアント経由でAPIMアプリに対してサインインすると、通常のOAuth 2.0同意画面で上記の委任許可が求められる。
      • ユーザーは一度の同意で、APIMが複数のバックエンドにアクセスする許可をまとめて与えることができる
      • 各バックエンドのスコープが列挙される。
    • 場合によっては管理者による事前同意を利用することで、ユーザー各々に同意を求めないことも可能

④Inboundセクションで、受信トークンの検証と新規トークン取得、そしてヘッダー差し替えを実装

  1. クライアントからAPIMに届いたアクセストークン(トークンA)を検証。
  2. validate-jwtポリシーで署名や有効期限をチェックし、不正なトークンであればリクエストを拒否。またaudクレームが自分(APIMアプリ)のクライアントIDになっていることも確認。
  3. 受けとったユーザートークン(JWT)の文字列を取り出し、後の処理で使えるよう変数に保存
  4. Entra IDのトークンエンドポイント(例: https://login.microsoftonline.com/<テナントID>/oauth2/v2.0/token )に対してHTTP POSTリクエストを送るようにポリシーを記述。リクエスト本体には以下の情報を指定する。
grant_typeurn:ietf:params:oauth:grant-type:jwt-bearer
JWT BearerトークンによるOAuth拡張フローであることを指定
client_idAPIMアプリのClient ID
client_secretAPIMアプリのClient Secret
assertion先ほど保存したユーザートークン
reqest_token_useon_behalf_of
scope呼び出し先バックエンドAPIが要求するスコープ
(例: api://<API_01のApp ID>/.default や 特定のReadスコープ等)
  1. Entra IDから返ってきた応答(JSON)から、バックエンド用アクセストークン(access_tokenフィールド)を取り出し、バックエンドに転送するリクエストのAuthorizationヘッダーを上書きして、ユーザーの代理トークンが付与された状態でリクエストをバックエンドAPIに中継する

これらをポリシーで実装すると以下のような感じ。

<policies>
 <inbound>
  <base />
  <!--User accessトークンを検証 - audの確認-->
  <validate-jwt header-name="Authorization" failed-validation-httpcode="401">
   <openid-config url="https://login.microsoftonline.com/<TenantId>/v2.0/.well-known/openid-configuration" />
   <required-claims>
    <claim name="aud">
     <value><APIMのClientId></value>
    </claim>
   </required-claims>
  </validate-jwt>
  <!--User accessトークンを取得し、変数として保持-->
  <set-variable name="UserToken" value="@(((String)context.Request.Headers["Authorization"][0]).Substring(7))" />
  <!--OBOトークンを取得 -->
  <send-request ignore-error="true" timeout="20" response-variable-name="oboResponse" mode="new">
   <set-url>https://login.microsoftonline.com/<TenantId>/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>@{
    return "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id={{APIMClientId}}&client_secret={{APIMClientSecret}}" +
    "&assertion=" + (string)context.Variables["UserToken"] + "&scope={{ScopeAPI01}}&requested_token_use=on_behalf_of";
    }</set-body>
  </send-request>
  <!--AuthorizationヘッダにBearerトークンをセット-->
  <set-header name="Authorization" exists-action="override">
   <value>@("Bearer " + (String)((IResponse)context.Variables["oboResponse"]).Body.As<JObject>()["access_token"])</value>
  </set-header>
 </inbound>
 ...
</policies>

⑤その他

JWTの検証においては、Entra IDが発行したものであれば、validate-azure-ad-tokenポリシーを使ってもよい。この場合は、以下のようなポリシー記述に変わる。

<validate-azure-ad-token tenant-id="{Tenant ID}" output-token-variable-name="jwt" failed-validation-httpcode="401">
  <required-claims>
    <claim name="aud" match="all">
      <value>{APIMアプリのClient ID}</value>
    </claim>
  </required-claims>
</validate-azure-ad-token>

2. User assigned managed identityを使う

1. ではClient IDとClient Secretを使っていたが、User assigned managed identityを使っても構成できる。この方法だと、Secretの管理をする必要がないため、より安全である(もちろん、1より2をおすすめした)。

①managed identityの割り当て

  • User assigned managed identityを作成し、それをAPIMインスタンスに関連付ける
    • System assigned managed identityではEntra IDのフェデレーション資格情報に登録できないため、この用途においてはuser assignedの一択になることに注意。
  • Azure Portalなどで、APIMのマネージドID設定を開き、作成したuser assigned managed identityをAPIMに割り当てる。

②アプリ登録

APIM、バックエンドサービスのアプリ登録はClient ID/Client Secretを使う場合と同じだが、以下の作業が異なる。

  • APIMに対しては、アプリ作成だけではなく、フェデレーション認証の設定を行う。
    • シークレットの代わりにmanaged identityを信頼させることで、Entra IDは指定のmanaged identityから発行されたアクセストークンをAPIMアプリのクライアント認証として受け入れることができる。

マネージド ID を信頼するようにアプリケーションを構成する / Configure an application to trust a managed identity
https://learn.microsoft.com/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity

  • APIMを表すEntra IDアプリ(フロントエンドからユーザーが同意を与え、APIMがバックエンドAPIアクセス許可を持つアプリ)の「フェデレーション資格情報」を追加
  • Azure PortalのEntra IDのアプリ登録画面からAPIMに該当するアプリを開き、「証明書とシークレット」の項目から「フェデレーション認証資格情報」を追加

③フェデレーション認証の設定

選択・入力すべきものは以下の通り。

IDプロバイダーmanaged identity
managed identityのタイプuser assigned managed identity
対象のmanaged identity先ほどAPIMに割り当てたmanaged identityのサブスクリプション、リソースグループ、リソース名を選択(またはClient IDを直接指定)
発行者 (issuer) やサブ (subject)Azure Portalでは自動設定
内部的にはこのマネージドIDによるトークン発行時のIssuerが特定のEntra IDテナント、SubjectはマネージドIDのObject ID

④トークン検証と交換

  • Client Secretを用いてEntra ID token endpointにPOSTしていた部分を、managed identityによるClient Assertion (client_assertion) 提供に書き換える
  • APIMのポリシーには、Azureリソースにアクセスするためのauthentication-managed-identityポリシーが用意されているので、これを利用してmanaged identityのaccess tokenを取得できる

ポリシーの構成は以下の通り。

  1. クライアントからAPIMに届いたaccess token(トークンA)をまず検証
  2. ポリシーを使って、署名や有効期限をチェックし、不正なトークンであればリクエストを拒否
  3. またaudクレームが自分(APIMアプリ)のクライアントIDであることも確認
  4. Entra IDからAPIMに割り当てたuser assigned managed identity用のaccess tokenを取得
    • send-requestポリシーでトークンエンドポイントに直接取りに行く代わりに、authentication-managed-identityポリシーを使う
    • このとき、resourceとして api://AzureADTokenExchange という特殊なリソース識別子を使う
  5. バックエンドAPI用アクセストークンを取得するため、Entra IDのトークン発行エンドポイントにリクエストを送信する
    • Client Secretの代わりに、先ほど取得したmanaged identity access tokenを用いてクライアント認証を行う
    • リクエスト本体には以下の情報を指定
client_idAPIMアプリのClient ID
assertionユーザーから受け取ったアクセストークン (JWT)
client_assertion_typeurn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion4で取得したmanaged identityのaccess token

Entra IDはこのリクエストを受け取ると、

  • client_assertionのトークンを検証
    • ここで先ほど設定したフェデレーション資格情報により、APIMに割り当てたmanaged identityから発行されたトークンがAPIMアプリからのクライアント認証で有効とみなされ、結果、Entra IDは「APIMに割り当てられたmanaged identityがEntra IDに自己証明する」形でクライアント認証に成功したと判断
  • その上でassertionに含まれるユーザートークンを検証
    • 従来と同じくOBOフローに従ってバックエンドAPI用アクセストークン(OBOトークン) を発行
  1. Entra IDから返ってきた応答(JSON)から、バックエンド用アクセストークン(access_tokenフィールド)を取り出し、バックエンドに転送するリクエストのAuthorizationヘッダーを上書きして、ユーザーの代理トークンが付与された状態でリクエストをバックエンドAPIに中継する

これらをポリシーで実装すると以下のような感じ。

<policies>
 <inbound>
  <base />
  <!--User accessトークンを検証 - audの確認-->
  <validate-jwt header-name="Authorization" failed-validation-httpcode="401">
   <openid-config url="https://login.microsoftonline.com/{{TenantId}}/v2.0/.well-known/openid-configuration" />
   <required-claims>
    <claim name="aud"><value>{{APIMClientId}}</value></claim>
   </required-claims>
  </validate-jwt>
  <!--User accessトークンを取得し、変数として保持-->
  <set-variable name="UserToken" value="@(((String)context.Request.Headers["Authorization"][0]).Substring(7))" />
  <!--マネージドIDトークンを取得-->
  <authentication-managed-identity 
   resource="api://AzureADTokenExchange" client-id="{APIMに割り当てたマネージドIDのクライアントID}" output-token-variable-name="miToken" />
  <!--OBOトークンを取得 -->
  <send-request ignore-error="true" timeout="20" response-variable-name="oboResponse" mode="new">
   <set-url>https://login.microsoftonline.com/{{TenantId}}/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>@{
    return "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id={{APIMClientId}}&client_assertion=" + 
        (string)context.Variables["miToken"] + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" +
    "&assertion=" + (string)context.Variables["UserToken"] + "&scope={{ScopeAPI01}}&requested_token_use=on_behalf_of";
    }</set-body>
  </send-request>
  <!--AuthorizationヘッダにBearerトークンをセット-->
  <set-header name="Authorization" exists-action="override">
   <value>@("Bearer " + (String)((IResponse)context.Variables["oboResponse"]).Body.As<JObject>()["access_token"])</value>
  </set-header>
 </inbound>
 ...
</policies>

これでおしまい。

アクセストークン取得時のパフォーマンス改善

できるとわかるとやはり欲が出るもので、以下のような更問をもらった。

OBO flowを実装できたのだが、バックエンドの呼び出し1回ごとにEntra IDへのトークン要求が発生するため、およそ数百ミリ秒のレイテンシーを要しているようだ。

  • 都度取得するのは効率が悪く、かつパフォーマンスへの影響も大きいのでなんとかする方法はないか?
  • トークン有効期限とトークンリフレッシュの方針に推奨があれば教えてほしい。

パフォーマンス改善

確かに大量のリクエストが来る場合はパフォーマンス上の負荷になるので、取得したアクセストークンを一時的にキャッシュすべきである。APIMはポリシーでcache-storeポリシーを使ってキャッシュ(内蔵またはAzure Cache for Redis等の外部キャッシュ)に値を保持できる。

例えば以下のような条件でトークンをキャッシュに保持すれば、同一ユーザーが短時間に連続してAPIコールする場合の不要なトークン再取得を避け、全体の応答時間を改善できる。

  • 「ユーザーIDと対象APIスコープ」をキーにしてトークンをトークンの有効期限内だけ保存
  • 次回以降のリクエストでは交換処理を省略してキャッシュから再利用

トークン有効期限とリフレッシュの方針

UXとセキュリティのトレードオフを考慮しつつ、寿命の設計を行う必要があるが、基本的には以下の方針を取るべきであろう。

  • アクセストークンの有効期限は短めに設定し、長期間有効なトークンが残らないようにする。
  • 一般的にアクセストークンは1時間程度だが、必要に応じてEntra IDの設定で寿命調整することを推奨。
  • OBO flow使用時、APIM自体はステートレスなゲートウェイとして機能するため、通常はリフレッシュトークンを保持しない構成にする。
    • したがって、クライアントはトークン期限が切れれば再度認証コードフローを実行する必要がある(その際もユーザー同意は一度きりで済む)。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください