C#アプリでWeb API呼び出しにAzure認証が必要な場合

# はじめに
Web API呼び出しにAzure認証が必要な場合、通常、APIのURLにアクセスすると認証画面にリダイレクトされてしまいます。Webコンポーネトを使って処理してもいいのですが、Microsoftが提供しているMSALライブラリを使うと簡単に処理できるようなので試してみました。

# 実現したいこと
外部公開していないWeb APIをAzure AD Application Proxy経由で、外部のC#のデスクトップアプリからアクセスする方法を考えます。
MSALライブラリの使い方を検索してもGraph API（マネージドサービス）にアクセスする例しか出てこないため、どうやっていいのかわからなかったのですが、以下の記事に載っていました。

https://docs.microsoft.com/en-us/azure/active-directory/app-proxy/application-proxy-configure-native-client-application

#MSALの使い方
サンプルが簡単なので甘くみていたのですが、落とし穴がありました。

## .Net Core
.Net Coreの場合、OSのブラウザを使用するのでアクセストークンを取得するためのシーケンスが以下のようになります。
まず、Azureポータルでリダイレクト先をlocalhost:xxxxに設定します。（後ほど説明します。）
アプリを起動してMSALのclientApp.AcquireTokenInteractiveを呼び出すと、
1. アプリがブラウザを起動する。
1. プラウザが認証画面を表示する。
1. ユーザが認証情報を入力する。
1. リダイレクトがlocalhost:xxxxに指定されているので、ブラウザは、localhostにアクセスする。
1. アプリは、localhost:xxxxで受信待ちになっているので、トークンのクエリーがついたHTTPリクエストを受け取る。 

この方法はあまり好ましくないので、Windows限定ですが、以下のページで紹介されている方法を使用した方がいいと思います。

https://blog.shibayan.jp/entry/20201130/1606730311

## .Net Framework
.Net Frameworkの場合、WebViewを使用するので内部的にWebViewのイベントフックでトークンを取得しているようです。

下記のコードは、MSのドキュメントに掲載されているサンプルコードを少し変更したものです。
"\<xxx>"は、Azureポータルから取得した値に置き換えます。

RedirectUriとscopesの設定でみごとにハマりました。

```CSharp
// Acquire Access Token from AAD for Proxy Application
IPublicClientApplication clientApp = PublicClientApplicationBuilder
.Create("<ClientId>")
// サンプルのままだとうまくいきませんでした。
.WithRedirectUri("<RedirectUri>") 
.WithAuthority("https://login.microsoftonline.com/{<Tenant ID>}")
.Build();

AuthenticationResult authResult = null;
var accounts = await clientApp.GetAccountsAsync();
IAccount account = accounts.FirstOrDefault();

//何を設定したらいいのかハマりました。
IEnumerable<string> scopes = new string[] {"<Scope>"};

try
 {
    authResult = await clientApp.AcquireTokenSilent(scopes, account).ExecuteAsync();
 }
    catch (MsalUiRequiredException ex)
 {
     authResult = await clientApp.AcquireTokenInteractive(scopes).ExecuteAsync();                
 }

if (authResult != null)
 {
  //Use the Access Token to access the Proxy Application

  HttpClient httpClient = new HttpClient();
  HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
  HttpResponseMessage response = await httpClient.GetAsync("<Proxy App Url>");
 }
```

### RedirectUri

.WithDefaultRedirectUri()だとHTTPのリターンで以下のようなエラーが返されてしまいます。
サンプルは、MSのGraph APIにアクセスすることしか考慮に入れていないようです。

```
"error":"invalid_client","error_description":"AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'
```

Azureポータル（アプリケーションゲートウェイ）で、「プラットフォームの構成」->「モバイル　アプリケーションとデスクトップアプリケーション」を追加し、
リダイレクトURLのmaslxxx-xxx-xxx://authにチェックを入れます。
このURLを”\<RedirectUri>”に設定します。

### scopes

次に、scopesですが正解にたどり着くまでかなりの時間がかかりました。
これに正しい値が設定されていないと、ログイン画面にリダイレクトされてしまいます。
以下のような、HTMLが返ってきます。

```html
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html dir="ltr" class="" lang="en">
<head>
    <title>Sign in to your account</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=2.0, user-scalable=yes">

続く
```

githubのIssuesでも同じ現象に悩まれていて、解決されずに放置されています。

Azureポータル（アプリケーションゲートウェイ）で、「プラットフォームの構成」->「APIアクセス許可」でuser_impersonationにチェックを入れます。

user_impersonationのAzureポータル側については、以下のドキュメントに書かれていました。
画面自体は、現在のものとは異なるようです。

https://docs.microsoft.com/ja-jp/azure/storage/common/storage-auth-aad-app

user_impersonationの設定はV1で、V2の設定方法も存在します。（V1とV2では、返されるトークンのJWTの中身が違うようです。）

こちらのドキュメントにもっと詳しく書かれています。user_impersonationについても触れられていますが、主にV2の内容です。

https://jpazureid.github.io/blog/azure-active-directory/oauth2-application-resource-and-api-permissions/

V1でもV2でもセキュリティ的に違いはないようなので、V1を使用します。（JWTトークンの中身は、アプリでは使用しないので。）

プログラムに記載する形式については、上記のドキュメントでは触れられていなかったのですが、MSの以下のサンプルのApp.configのコメントに書かれていました。

https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/2.%20Web%20API%20now%20calls%20Microsoft%20Graph

"\<Scope>"に"\<ClientId>/user_impersonation"を設定します。

# まとめ

MSのドキュメントやサンプルコードがGraph APIにアクセスすることしか考えられていなかったためエラーに悩まされました。
独自のWeb APIを呼び出す際のRedirectUriとscopesについてちゃんと説明してくれていたらこんなに苦労しなかったと思います。
また、.Net CoreのケースもMSのドキュメントでは~~説明不足で~~理解できませんでした。
詳しく説明しているドキュメントが見当たらなかったので、困っている人のお役に立てたら幸いです。