はまったりひらめいたり…とか…

Angularや.NETやAzureやその他色々。

YARPを利用して実装したWeb APIをAzure ADで保護してOn-Behalf-Ofフローでトークンを再取得し通信する

はじめに

この記事は2021年11月に記述された内容です。

参照時期によっては記載している実装内容と異なっている可能性がありますのでその点ご留意ください。

記事中の実装内容はYarp.ReverseProxyの1.0.0をベースに実装されています。

YARP

Visual Studio 2022 Launch 記念 C# Tokyo イベントの発表中にYARPがリリースされたことが触れられていました。

触ってみたところ、欲しかったものだったので公式Docsに記載されている内容以外で少し検証したことを軽くまとめておこうと思います。

そもそもYARPとは

Yet Another Reverse ProxyでYARPらしいです。

名前の通りリバースプロキシで、ASP.NET Core上でプロキシルーティングや処理動作を簡易に使用したりカスタマイズしたりできるライブラリのようです。

具体的には下記のようにappsetting.json内に記述することでhttp://example.com/proxyone/proxyapiにアクセスするとhttp://other.example.com/proxyapiにアクセスしデータが取得されます。

{
...
  "ReverseProxy": {
    "Routes": {
      "proxy-one-route": {
        "ClusterId": "proxy-one-api",
        "Match": {
          "Path": "/proxyone/{*any}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/proxyone" }
        ]
      }
    },
    "Clusters": {
      "proxy-one-api": {
        "Destinations": {
          "destination1": {
            "Address": "http://other.example.com/"
          }
        }
      }
    }
  }
...
}

どのような事ができるのか、またどのように実装すれば良いのかは公式のドキュメントYARPのGitHubのリポジトリsamplesサンプルソースが多数あります。非常に参考になりました。

検証「YARPで実装したWeb APIをAzure ADで保護してOn-Behalf-Ofフローでトークンを再取得する」

なにをしたのか

YARPの変換処理でOn-Behalf-Ofフローができるか確かめました

なぜそのようなことをしたのか

比較的社内アプリのようなものを作ることが多いため、Web APIMicrosoft.Identity.Webを使用し、Azure ADで保護することが多いです。

そのため、YARPで実装したWeb API(以降YARP APIと呼称します)もAzure ADで保護を行いたいと考えました。

また、YARP APIから接続するWeb APIも別のAzure ADアプリケーションで保護されたWeb APIMicrosoft Graphである可能性があるため

YARP APIの中でアクセストークンを適切に付与し直したいと欲求がありました。

AADアプリで保護したWeb APIから別のAADアプリで保護したWeb APIへのアクセスにOn-Behalf-Ofフローを使うことはしょっちゅうあるため、今回の検証を行いました。

実装内容

まずはYARP APIの保護

Microsoft.Identity.Webを使用して、Startup.csで設定するおなじみの方法で保護できました。

On-Behalf-OfフローでITokenAcquisitionを使用するためにEnableTokenAcquisitionToCallDownstreamApiを使用したりするのもおなじみの方法です。

public void ConfigureServices(IServiceCollection services)
{
  services.AddMicrosoftIdentityWebApiAuthentication(Configuration).EnableTokenAcquisitionToCallDownstreamApi().AddInMemoryTokenCaches();
}

参考:damienbod/AspNetCoreYarp

これで、YARP APIにアクセスするさいにアクセストークンがない場合401が返却されるようになりました。

特定のパスのときにOn-Behalf-Ofフローする

公式のサンプルReverseProxy.Auth.Sampleを参考にしました。公式ドキュメントではTransformと呼ばれている処理を実装します。

まずはルーティング時にTransformするよう設定

appsetting.jsonは下記のように設定しました。

{
...
  "ReverseProxy": {
    "Routes": {
      "proxy-one-route": {
        "ClusterId": "proxy-one-api",
        "AuthorizationPolicy": "default",
        "Match": {
          "Path": "/proxy-one-api/{*any}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/proxy-one-api" },
          { "OnBeHalfOfProxy": "xxxxx" }
        ]
      }
    },
    "Clusters": {
      "proxy-one-api": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:44352/api/"
          }
        }
      }
    }
...
}

proxy-one-apiのパスが来たときにproxy-one-apiのパスをリムーブ、今回実装したOnBeHalfOfProxyの変換を実行の後、https://localhost:44352/api/にアクセスされます。

OnBeHalfOfProxyの変換処理において、取得先のスコープ情報がほしいので、マスクしていますがValue値にはスコープ情報が記述されます。

Startup.csでカスタムTransformを使用するよう設定

OnBeHalfOfProxyStartup.csで設定されます。

public void ConfigureServices(IServiceCollection services)
{
...
  var proxyBuilder = services.AddReverseProxy().AddTransformFactory<ProxyAuthFactory>();
  proxyBuilder.LoadFromConfig(Configuration.GetSection("ReverseProxy"));
...
}

AddTransformFactoryで今回実装したオリジナルのTransformの実装であるProxyAuthFactoryを追加しています。

カスタムTransformの実装

ProxyAuthFactoryは下記のような実装です。実装している内容は記載しているコメントのとおりです

我ながらだいぶ雑味が強いので参考程度に…。

public class ProxyAuthFactory : ITransformFactory
{

    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)
    {
        // appsetting.jsonで設定するTransformの識別子"OnBeHalfOfProxy"
        if (transformValues.TryGetValue("OnBeHalfOfProxy", out var scope))
        {
            if (string.IsNullOrEmpty(scope))
            {
                throw new ArgumentException("A non-empty OnBeHalfOfProxy value is required");
            }
            context.AddRequestTransform(async transformContext => {
                // Transform時に実行される処理

                // Microsoft.Identity.WebのITokenAcquisitionを使用してアクセストークンを取得し直す。
               // 普通に使用する場合はDIを使用するが、タイミング的にコンストラクタで読み込めないためここでインスタンスを取得する
                var tokenAcquisition = transformContext.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
               // いつものOn-Behalf-Ofフローのアクセストークン取得処理
                var res = await tokenAcquisition.GetAccessTokenForUserAsync(new List<string> { scope });
               // Authorizationヘッダにアクセストークンを付与(Transform)
                transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", res);
            });
            return true;
        }
        return false;
    }

    // appsetting.jsonでこのTransformを使用する設定があった場合行われるチェック処理のようだ
    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("OnBeHalfOfProxy", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
               // アクセストークンの再取得にスコープ情報がほしいので、Valueを必須にした。
                context.Errors.Add(new ArgumentException("A non-empty OnBeHalfOfProxy value is required"));
            }

            return true; // Matched
        }
        return false;
    }
}

これでproxy-one-apiにアクセスした際に、指定したスコープでアクセストークンが再取得されるようになりました。

例えば、{ "OnBeHalfOfProxy": "user.read" }としたときはuser.readスコープでアクセストークンが再取得されるようになるので

「このパスのときはこのスコープでアクセストークン再取得して」といった設定が非常に簡単にできるようになっています。

APIMなんかで同じことをしたときは、XML上にC#スクリプトでゴニョゴニョする感じだったので、それと比べるとこちらのほうが個人的には好みです。

さいごに

というわけで、軽くYARPを触ってみたメモ書きでした。

APIMはお高すぎる&機能がFatすぎる的な理由で、独自に色々実装したりしていましたが、これを使用することで色々きれいに運用できそうな気がします。

今回検証に使用したソースコードは下記に格納されています。On-Behalf-Ofはもちろんですが

GET~DELETEとか、Formデータ送った場合とか、IFormFile受け取れるのかとか、DELETEでbody突っ込んでみたりとか色々実験したりしています。

github.com