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 APIはMicrosoft.Identity.Web
を使用し、Azure ADで保護することが多いです。
そのため、YARPで実装したWeb API(以降YARP APIと呼称します)もAzure ADで保護を行いたいと考えました。
また、YARP APIから接続するWeb APIも別のAzure ADアプリケーションで保護されたWeb APIやMicrosoft 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(); }
これで、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を使用するよう設定
OnBeHalfOfProxy
はStartup.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突っ込んでみたりとか色々実験したりしています。