ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

ASP.NET Core アプリケーションを Azure Active Directory v2.0 で認証する

Azure Active Directory(Azure AD)の v2.0 は Microsoft ID プラットフォームと呼ばれていて、OpenID Connect 準拠のエンドポイントが提供されていたり、アクセス許可を一度にまとめて確認するでのはなく、必要に応じてアクセス許可を確認(増分および動的な同意)できたりします。
ただし、Azure AD v1.0 のすべての機能が v2.0 で提供されているわけではないので、注意が必要です。
docs.microsoft.com
今回は、ASP.NET Core で作った Web アプリケーションと Web API に Azure AD v2.0 認証を組み込み、フロントエンドの Web アプリケーションからバックエンドの Web API を呼び出してみます。
基本的にはこちらのサンプル通りですが、必要最低限のコードで試してみました。
github.com

事前準備

Visual Studio 2019 のプロジェクトテンプレートを使って、ASP.NET Core 3.1 で Web アプリケーション(MVC)と Web API のプロジェクトを作成しておきます。
テンプレートで認証を設定すると、Azure AD v1.0 向けの Active Directory Authentication Library(ADAL)が使われるので、今回は指定しません。
Azure AD v2.0 向けの Microsoft Authentication Library(MSAL)を手動で組み込みます。現時点のバージョンは 0.3.1-preview となっており、プレビュー版です。
docs.microsoft.com

Azure AD にアプリケーションを登録する

Azure Portal で Azure AD のアプリケーションを2つ登録します。

  • フロントエンドの Web アプリケーション
  • バックエンドの Web API
必須項目 設定値
Redirect URIs https://localhost:xxxxx/signin-oidc(Web アプリと Web API の URL)
Implicit grant ID tokens にチェックをいれる
Client secrets 自動生成(後から確認できないのでメモしておく)

バックエンドの Web API のみ、Expose an API メニューから Application ID URIScopes を登録します。
f:id:TonyTonyKun:20200920165840p:plain

フロントエンドの Web アプリケーションを作る

Web アプリケーションのプロジェクトに対して、MSAL の NuGet パッケージをインストールします。

PM>Install-Package Microsoft.Identity.Web.UI -Version 0.3.1-preview

Azure AD のアプリケーションに登録した情報をもとに、appsettings.json に AzureAd のセクションを追加します。

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "[テナントのドメインを入力します。e.g. contoso.onmicrosoft.com]",
    "TenantId": "[Directory (Tenant) ID を入力します。]",
    "ClientId": "[Application (Client) ID を入力します。]",
    "CallbackPath": "/signin-oidc",
    "ClientSecret": "[Client secrets を入力します。]"
  },
}

Startup クラスにおいて、MSAL の初期化処理を実装します。

public void ConfigureServices(IServiceCollection services)
{
    // appsettings.json の AzureAd セクションから構成を読み取り、認証サービスを構成します。
    services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd")
            .EnableTokenAcquisitionToCallDownstreamApi()
            .AddInMemoryTokenCaches();

    services.AddHttpClient();

    services.AddControllersWithViews()
                .AddMicrosoftIdentityUI();    // アカウント管理ページを追加
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // (ここまでのコードは省略)

    app.UseRouting();
    app.UseAuthentication();    // パイプラインに認証ミドルウェアを追加
    app.UseAuthorization();

    // (これ以降のコードは省略)
}

HomeController クラスには、Authorize 属性を指定します。

[Authorize]
public class HomeController : Controller
{
}

ログインしたユーザー名とサインアウトをヘッダーに追加しておきます。Views/Sharedディレクトリに _LoginPartial.cshtml を追加し、_Layout.cshtml に組み込みます。
asp-area には、MicrosoftIdentity を指定します。

@using System.Security.Principal

<ul class="navbar-nav">
@if (User.Identity.IsAuthenticated)
{
  <li class="nav-item">
      <span class="navbar-text text-dark">Hello @User.Identity.Name!</span>
  </li>
  <li class="nav-item">
      <a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a>
  </li>
}
else
{
  <li class="nav-item">
      <a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
  </li>
}
</ul>

以上で、フロントエンドの Web アプリケーションは実装完了です。Visual Studio からデバッグ実行して、動作確認してみます。

f:id:TonyTonyKun:20200920173849p:plain

バックエンドの Web API を作る

Web API のプロジェクトに対して、MSAL の NuGet パッケージをインストールします。

PM>Install-Package Microsoft.Identity.Web -Version 0.3.1-preview

Azure AD のアプリケーションに登録した情報をもとに、appsettings.json に AzureAd のセクションを追加します。
Web アプリケーションと同様ですが、API なので CallbackPath は必要ありません。

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "[テナントのドメインを入力します。e.g. contoso.onmicrosoft.com]",
    "TenantId": "[Directory (Tenant) ID を入力します。]",
    "ClientId": "[Application (Client) ID を入力します。]",
    "ClientSecret": "[Client secrets を入力します。]"
  },
}

Startup クラスにおいて、MSAL の初期化処理を実装します。

public void ConfigureServices(IServiceCollection services)
{
    // appsettings.json の AzureAd セクションから構成を読み取り、認証サービスを構成します。
    services.AddMicrosoftIdentityWebApiAuthentication(Configuration, "AzureAd");

    services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // (ここまでのコードは省略)

    app.UseRouting();
    app.UseAuthentication();    // パイプラインに認証ミドルウェアを追加
    app.UseAuthorization();

    // (これ以降のコードは省略)
}

WeatherForecastController クラスには、Authorize 属性を指定します。

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
}

以上で、バックエンドの Web API は実装完了です。Visual Studio からデバッグ実行して、Postman で動作確認してみます。

f:id:TonyTonyKun:20200920174759p:plain

Postmanを使ったアクセストークンの取得は、こちらの記事を参照してください。
gooner.hateblo.jp

フロントエンドからバックエンドの Web API を呼び出す

Azure AD のアプリケーションを変更する

フロントエンドの Web アプリケーションに対して、バックエンドの Web API を呼び出せるアクセス権を追加します。

f:id:TonyTonyKun:20200921115951p:plain

API のアクセス権は、add a permission をクリックして選択できます。忘れずに、Grant admin consent for 既定のディレクトリも設定しておきます。

フロントエンドの Web アプリケーションを変更する

Web アプリケーションのプロジェクトに対して、appsettings.json に CallApi のセクションを追加します。

{
  "CallApi": {
    "Scope": "[Web API の Scope を入力します。e.g. api://<GUID>/user_impersonation]",
    "BaseAddress": "[Web API の URL を入力します。e.g. https://localhost:xxxxx]"
  }
}

Startup クラスにおいて、MSAL の初期化処理にバックエンドの Web API の Scope を追加します。EnableTokenAcquisitionToCallDownstreamApi メソッドの引数に Scope を渡します。

public void ConfigureServices(IServiceCollection services)
{
    // バックエンドの Web API の Scope を追加
    services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
            .EnableTokenAcquisitionToCallDownstreamApi(new string [] { Configuration["CallApi:Scope"], })
            .AddInMemoryTokenCaches();
}

バックエンドの Web API を呼び出すアクセストークンを取得したいので、HomeController クラスのコンストラクタで ITokenAcquisition のインスタンスを DI コンテナから受け取ります。

[Authorize]
public class HomeController : Controller
{
    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly IHttpClientFactory _clientFactory;
    private readonly string _webApiScope = string.Empty;
    private readonly string _webApiBaseAddress = string.Empty;

    public HomeController(ITokenAcquisition tokenAcquisition, IConfiguration configuration, IHttpClientFactory clientFactory)
    {
        _tokenAcquisition = tokenAcquisition;
        _clientFactory = clientFactory;
        _webApiScope = configuration["CallApi:Scope"];
        _webApiBaseAddress = configuration["CallApi:BaseAddress"];
    }
}

Privacy ページがクリックされた際に、バックエンドの Web API を呼び出します。アクションメソッドには、AuthorizeForScopes 属性でバックエンドの Web API の Scope を付与します。

[AuthorizeForScopes(ScopeKeySection = "CallApi:Scope")]
public async Task<IActionResult> Privacy()
{
    // アクセストークンを取得する
    var scopes = new string[] { _webApiScope };
    var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);

    // バックエンドの Web API を呼び出し
    var client = _clientFactory.CreateClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    var response = await client.GetAsync($"{_webApiBaseAddress}/weatherforecast");
    if (response.StatusCode == HttpStatusCode.OK)
    {
        ViewData["ApiResult"] = await response.Content.ReadAsStringAsync();
    }
    else
    {
        ViewData["ApiResult"] = $"Invalid status code in the HttpResponseMessage: {response.StatusCode}.";
    }
    return View();
}

実行結果

Azure AD で認証されたフロントエンドの Web アプリケーションから、バックエンドの API を呼び出せることを確認します。

f:id:TonyTonyKun:20200921123219p:plain

まとめ

ASP.NET Core で作った Web アプリケーションと Web API に Azure AD v2.0 認証を組み込み、フロントエンドの Web アプリケーションからバックエンドの Web API を呼び出してみました。
認証ライブラリの MSAL はまだプレビュー版ですし、もうすぐ .NET 5 がリリースされるタイミングなので、継続してキャッチアップしていきたいと思います。
今回のソースコードは、こちらで公開しています。
github.com