📑

Azure ADでログインしてBlazor WASM→ASP.NET Core WebAPI→MS Graph APIを呼びたい

2022/11/22に公開約8,400字

SPA のようなアプリを作っているとクライアントサイドでログイン機能を実装して、そこで取得したアクセストークンンをつけてサーバーサイドの Web API を呼び出して、Web API からさらに別の Web API を呼び出すというような流れを組みたくなることがあります。

仕事柄、私は末端の API は Microsoft Graph API になったりするのですが、Microsoft Graph API は /me のようなエンドポイントがあって、叩いた人の情報が取れたりします。
その他にも、自分のメールや予定なんかが取れたりするのですが、このように Web API 側からクライアントでログインした人のかわりに Microsoft Graph API を叩きたいという状況がボチボチあります。

On-Behalf-Of フロー

そういう時に使うといいよって案内されているのが OAuth 2.0 の On-Behalf-Of フローになります。

https://learn.microsoft.com/ja-jp/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow

因みにドキュメントの最後にも書かれていますが、クライアントとサーバーでわかれてはいるけど単一のアプリケーションとして扱えるのであれば扱った方がシンプルではあります。(下記引用を参照)

単一アプリとして扱う部分の引用

一部のシナリオでは、中間層クライアントとフロントエンド クライアントの単一ペアのみ使用する場合があります。 このシナリオでは、これを単一アプリケーションにする方が簡単で、中間層アプリケーションをまったく必要としない場合があります。 フロントエンドと Web API 間で認証を行うために、アプリケーション自体に要求された cookie、id_token、またはアクセス トークンを使用できます。 その後、この単一アプリケーションからバックエンド リソースへの同意を要求します。

ですが、今回は勉強のため別建てアプリとして作って On-Behalf-Of フローでやってみようと思います。

今回使うもの

今回はここら辺を使って作っていこうと思います。

  • クライアント サイド
    • ASP.NET Core Blazor WebAssembly
  • サーバー サイドの Web API
    • ASP.NET Core Web API
  • Web API から呼び出す先の API
    • Microsoft Graph API

認証は Azure AD 認証で行きます。

大雑把な処理の流れ

ということで今回作るのはこんな処理の流れのものになります。
本当は Web API も認証で保護されているので受け取ったトークンの検証はしてるし、ログイン時はちゃんと AAD のログイン画面が表示されてユーザー自身の手で ID/Pass などを入れて認証していたりといったフローになりますが、厳密に書くと矢印の数がえぐいことになるのでざっくりと。

AAD にアプリ登録を作ろう

では、Azure AD にアプリを 2 つ登録していきます。以下のような流れになります。

  1. クライアント用アプリの登録
  2. サーバー用アプリの登録
  3. サーバーアプリにクライアントから呼び出し可能にするためのスコープを定義する
  4. クライアント用アプリにサーバー用アプリを呼び出すアクセス許可を追加する

クライアント用アプリ

まずはクライアントの ASP.NET Core Blazor WebAssembly で使うアプリ登録を作っていきます。Azure AD のポータルからアプリ登録でアプリ登録を作り認証の項目のプラットフォームにシングルページアプリケーションを追加してリダイレクト URL に https://localhost/authentication/login-callback を設定します。今回はローカルで動かして動作させようとしているので localhost になっていますが、何処かのサーバーにデプロイする場合は localhost の部分は、デプロイ先のドメインを入れてください。

サーバー用アプリ

サーバー用アプリも作成します。
こちらはリダイレクト URL は不要なのですが、クライアント側のアプリに公開するスコープを作るといったことが必要になります。

まずは、適当な名前でアプリを作成します。

API の公開のページでアプリケーション ID の URL を作成して access_as_user のスコープを追加します。

次に、API のアクセス許可に移動して AD に管理者の同意を与えます を選択して管理者の同意を与えておきます。

クライアント用アプリの構成

クライアント用アプリのアプリ登録を開いて API のアクセス許可を開きます。
アクセス許可の追加から「自分の API」を選んで一覧の中からサーバー用アプリの名前を選びます。

先ほど作った access_as_user が表示されているのでチェックをいれてアクセス許可の追加を押します。

そして AD に管理者の同意を与えます を選択して管理者の同意を与えておきます。

プログラミング…!

では順番にプログラムを作っていきます。

サーバーサイドの作成

サーバーサイドを ASP.NET Core Web API で作成します。(別に他のでもいいのですがなんとなく)

ポイントは認証をサポートするために認証の種類を Microsoft ID プラットフォームを選択するところです。

認証をするように設定してプロジェクト作成を進めると以下のように、どのアプリと紐づけるかという選択をする画面が出てくるのでサーバー用に作成したアプリ登録を選択します。

次の画面で、Microsoft Graph API を呼ぶチェックを入れます。

次の画面でシークレットの保存について聞かれるのでそのまま完了します。
これで認証で保護された API (/weatherforecast) を持ったアプリの完成なのですが、次に作成する Blazor WASM のクライアントアプリからの呼び出しに対応するために CORS の構成を追加しておきます。

Program.cs を開いて AddCorsUseCors を追加します。

Program.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

// 追加
builder.Services.AddCors(options =>
{
    // ザル設定注意!
    options.AddDefaultPolicy(builder => builder.AllowAnyHeader().AllowAnyOrigin());
});
// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
        .EnableTokenAcquisitionToCallDownstreamApi()
            .AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
            .AddInMemoryTokenCaches();

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseCors(); // 追加

app.UseAuthentication();

app.UseAuthorization();

app.MapControllers();

app.Run();

クライアントの作成

次にクライアントを作成します。同じソリューションに ASP.NET Core Blazor WebAssembly のプロジェクトを追加します。

こちらにも認証機能を追加するので、プロジェクトの作成画面で認証の種類に Microsoft ID プラットフォームを選択します。

サーバー側と同様に紐づけるアプリケーションに先ほど作ったアプリを選択します。

次の画面で別の API にアクセス許可を追加するという項目にチェックを入れてサーバーサイドのプロジェクトを選択します。そうすると自動的に API URL と API で定義されるスコープが入力されます。

次のページでクライアントシークレットについて聞かれるのですが不要なのでなしにしておきます。

次のページで完了を選択すると認証が構成されたアプリケーションが作成されます。

作成したクライアントのプロジェクトをスタートアッププロジェクトに設定して実行すると以下のように Log in のリンクがついた画面が表示されます。

Log in リンクを選択するとログインのプロンプトが表示されるので、アプリを登録したテナントに存在するユーザーでログインを行うと以下のようにログインしたユーザーの名前が表示されます。

全体の疎通確認

次にクライアントからサーバーの API を呼んでみたいと思います。といってもクライアント側の FetchData.razor のコードはサーバーの weatherforecast エンドポイントを叩くように構成されているので両方のプロジェクトを起動するだけで動くようになっています。

ソリューションのプロパティ画面からマルチ スタートアップ プロジェクトでサーバー側とクライアント側の両方のプロジェクトを起動するように設定します。

実行してログインして Call Web API のページを開くと以下のような画面が表示されます。

この表示されているデータはサーバーサイドのプロジェクトの WeatherForecastController で生成されたデータになります。ブレークポイントを置いておくことで確認できます。

では、最後に Microsoft Graph API を呼び出して、ちゃんと呼べているか確認してみましょう。

といっても実は WeatherForecastController には既に Graph API の呼び出しコードが追加されています。1 つ上の画像の 30 行目で var user = await _graphServiceClient.Me.Request().GetAsync(); となっている箇所がそれです。

デバッグ用に WeatherForecastController のコードを以下のように書き換えてみましょう。

WeatherForecastController.cs
    [HttpGet(Name = "GetWeatherForecast")]
    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        var user = await _graphServiceClient.Me.Request().GetAsync();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = $"{Summaries[Random.Shared.Next(Summaries.Length)]}({user.DisplayName})",
        })
        .ToArray();
    }

Summaryuser.DisplayName も埋め込みました。ポイントはクライアントサイドではアクセストークンの取得に Graph API 用の User.Read は指定していない点です。サーバーサイドで OBO フローで新たにアクセストークンを取得して Graph API を呼び出している所になります。その状態でもきちんとログインしているユーザー名が取れているかどうかという所が確認のポイントです。

プログラムを再実行してログインして Call Web API ページを開きます。きちんと Summary にユーザー名が表示されています。

コード上で見るべきポイント

ここまで、基本的にはプロジェクトの新規作成時に設定をしてぽちぽちやっていると動くものが出来上がってしまっていました。

プロジェクト作成時に設定した情報や、それに応じてコードが追加されているポイントとなる箇所をピックアップしてみました。

サーバーサイド

  • Program.cs
    • AddAuthentication から始まる一連のメソッドチェーンが追加されたコードです
    • 下にある appsettings.json の AzureAd セクションや MicrosoftGraph セクションの内容
  • appsettings.json
    • プロジェクトの作成時に選択したアプリの ID やテナントの情報が設定されています。
  • Secrets.json (プロジェクトの右クリックでユーザーシークレットの表示から確認できます)
    • シークレットが設定されています
  • Controllers/WeatherForecastController.cs
    • Authorize 属性と RequiredScope 属性が追加されています

クライアントサイド

  • Program.cs
    • AddMsalAuthentication の呼び出し部分が追加されたコードです。
    • 12 行目からの AddScoped メソッドでアクセストークンつきの HTTP リクエストを送るための HttpClient を設定しています
    • appsettings.json に追加された AzureAd や DownstreamApi セクションの情報をもとに認証の設定が行われています。
  • wwwroot/appsettings.json
    • プロジェクトの作成時に選択したアプリの ID やテナントの情報が設定されています。
    • DownstreamApi に呼び出し先のサーバーのアプリの情報が設定されています。
  • Pages/FetchData.razor
    • Authorize 属性が追加されています
    • Web API を呼び出すように変更されています
  • App.razor
    • CascadingAuthenticationState や AuthorizeRouteView を使って認証を考慮した画面遷移などが行われるように設定されています

まとめ

認証系は難しいですが、プロジェクトテンプレートである程度動くものが出来上がってくれるので、これで動きを確認したうえで ASP.NET Core の認証系のドキュメントを読むと理解が深まると思います。

悩ましいのが、テンプレートの癖に色々沢山コードを吐き出してくれるので、本番で使うコードの土台にするには、生成されたプロジェクトから色々コードを削ったりカスタマイズしないといけないので、それはそれでメンドクサイところでしょうか…。

Discussion

ログインするとコメントできます