Azure AD B2C×ASP .NET CoreのAPIキー認証

Azure AD B2Cのカスタムポリシーを使えば、REST APIの呼び出しにより独自のビジネスロジックを一連の認証フローの中で行うことができます。
REST API呼び出しのチュートリアルは下記の通りです。
[チュートリアル:Azure Active Directory B2C で REST API 要求の交換をカスタム ポリシーに追加する](https://docs.microsoft.com/ja-jp/azure/active-directory-b2c/custom-policy-rest-api-claims-exchange)

ただし上記チュートリアルのドキュメントに下記の記載がある通り、チュートリアルのコードのままですとB2C側は保護されていないREST APIに接続できてしまいます。
> お使いの RESTful API を実稼働用にセキュリティで保護する方法については、RESTful API のセキュリティ保護に関するページを参照してください。

そのため、実運用する際はREST APIの保護を行うようB2C側とAPI側両方に対応が必要です。
REST APIの保護についてはいくつか方法がありますが、今回はAPIキー認証の方法をご紹介します。

* 認証方法の詳細については[こちら](https://docs.microsoft.com/ja-jp/azure/active-directory-b2c/secure-rest-api)の公式ドキュメントをご参照ください。


### この記事のターゲット
* Azure AD B2Cで独自のREST API呼び出しを利用される方
* Azure AD B2Cを実運用するアプリに組み込みたい方
* ASP .NET CoreでREST API開発をされている方
  * 本記事で紹介する認証方法はHTTPリクエストヘッダー内のAPIキーを照合する一般的なAPIキー認証のため、Azure FunctionsやJava Springなど他のREST APIフレームワークでも対応できます。

### この記事のゴール
* B2C～REST API間のAPIキー認証を利用するための設定およびカスタムポリシーを変更する。
* ASP .NET Coreで、APIキーの認可処理を行う。
* APIキー認証のエラー時に、エラーメッセージを返す。

### 前提
* B2Cではカスタムポリシーを使用します。
  * B2Cの認証フローの定義方法にはユーザーフローとカスタムポリシーの2つがあり、MS公式ではユーザーフローの利用が推奨されています。
  * ただし、独自のREST APIの組み込みはカスタムポリシーのみのサポートであるため、今回はそちらを使用します。
* REST API側のフレームワークはASP .NET Core 3.1を使用します。

### B2C側の設定
B2C側では、ポリシーキーの追加とカスタムポリシーの変更を行います。
詳細なドキュメントは[こちら](https://docs.microsoft.com/ja-jp/azure/active-directory-b2c/secure-rest-api#api-key-authentication)の公式ドキュメントをご参照ください。

#### ポリシーキーを作成する
下記の通り、B2Cの設定画面でポリシーキーを作成します。
キャプチャの`api-key-value`の箇所にAPIキーを設定します。
![B2C1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398865/284ed8a4-995e-acd3-2ad3-d82d05670f38.png)
作成後、ポリシーキー一覧画面にこのように追加されます。設定した名前に`B2C_1A_`プレフィックスが付与されます。
このキー名はカスタムポリシーで使用するため名前をあらかじめコピーしておきます。
![B2C2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398865/c7ea5ba7-546d-c644-4fe7-e70ef3b471bb.png)

#### カスタムポリシーを変更する
REST API呼び出し部分の`TechnicalProfile`を変更します。
例として、以下のソースコードをベースに改修していきます。

```xml
<TechnicalProfile Id="TestApi">
  <Metadata>
    <Item Key="ServiceUrl">https://xxx.example.com/api/auth/user</Item>
    <Item Key="AuthenticationType">None</Item>
    <Item Key="SendClaimsIn">Body</Item>
    <Item Key="AllowInsecureAuthInProduction">true</Item>
  </Metadata>
  ...
</TechnicalProfile>
```

変更ポイントは下記の3つです。

* `Metadata`の`AuthenticationType`を`ApiKeyHeader`に変更します。
* `Metadata`の`AllowInsecureAuthInProduction`を`false`に変更します。
* `CryptographicKeys`を追加し、APIキーを下記の通り設定します。
    * `Id`：ヘッダーのキー名
    * `StorageReferenceId`：ポリシーキー名
      * 「[ポリシーキーを作成する](#ポリシーキーを作成する)」で作成したキー名を設定します。
    * 下記コードの例ですと、REST API呼び出し時`x-api-key`ヘッダーに`B2C_1A_RestApiKey`ポリシーキーの値を格納します。

```xml
<TechnicalProfile Id="TestApi">
  <Metadata>
    <Item Key="ServiceUrl">https://xxx.example.com/api/auth/user</Item>
    <Item Key="AuthenticationType">ApiKeyHeader</Item>
    <Item Key="SendClaimsIn">Body</Item>
    <Item Key="AllowInsecureAuthInProduction">false</Item>
  </Metadata>
  <CryptographicKeys>
    <Key Id="x-api-key" StorageReferenceId="B2C_1A_RestApiKey" />
  </CryptographicKeys>
  ...
</TechnicalProfile>
```

### REST API側の設定（ASP .NET Core 3.1）
B2Cから受け取ったリクエストヘッダー内のAPIキーを照合し、既定値と異なる場合はエラーを返します。
これを実現するには、独自の認可処理を行うAttributeを作成し、B2Cのリクエストを受け付けるAPIコントローラーに適用させます。

また、ASP .NET Coreの標準ライブラリのみを利用するため、別途ライブラリのインポートは不要です。

* ApiKeyAuthorizeFilterクラス
  * [IAuthorizationFilter](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.aspnetcore.mvc.filters.iauthorizationfilter?view=aspnetcore-3.1)をオーバーライドし、独自の認可用フィルターを作成します。
  * B2C側から受け取ったリクエストヘッダーを照合し、既定値と異なる場合はエラーを返します。
  * ポイントはエラーメッセージの返し方で、`ApiKeyError()`メソッドでB2C規定の形式でエラーレスポンスで返すようにしています。
    * 本来認証エラーは`401 Unauthorized`として返しますが、B2Cでサポートされているエラーレスポンスは`409 Conflict`のため`ConflictObjectResult`を返しています。
    * その他、エラーレスポンスの詳細については[こちら](https://docs.microsoft.com/ja-jp/azure/active-directory-b2c/restful-technical-profile#returning-validation-error-message)をご参照ください。
 
  ```csharp
  using Microsoft.AspNetCore.Mvc;
  using Microsoft.AspNetCore.Mvc.Filters;
  using Microsoft.Extensions.Configuration;
  using System;
  using System.Linq;
  using System.Net;

  namespace SampleApi.Auth.Filters
  {
      public class ApiKeyAuthorizeFilter : IAuthorizationFilter
      {
          public IConfiguration Configuration { get; }

          /// <summary>
          /// APIキーのリクエストヘッダー名
          /// </summary>
          public static readonly string ApiKey = "x-api-key";

          public ApiKeyAuthorizeFilter(IConfiguration configuration)
          {
              Configuration = configuration;
          }

          /// <summary>
          /// 認可処理
          /// </summary>
          /// <param name="context"></param>
          public void OnAuthorization(AuthorizationFilterContext context)
          {
              // リクエストヘッダーのAPIキーを元に認可処理
              var apiKeys = context.HttpContext.Request.Headers[ApiKey];
              if (apiKeys.Contains(Configuration["AuthorizedKey"]))
              {
                  return;
              }
              // 既定値と一致しない場合はエラーを返す
              context.Result = ApiKeyError();
          }

          /// <summary>
          /// APIキー不正エラーメッセージを返す
          /// </summary>
          /// <returns></returns>
          private ConflictObjectResult ApiKeyError()
          {
              return new ConflictObjectResult(new 
              {
                  Version = "1.0.0",
                  Status = HttpStatusCode.Conflict,
                  UserMessage = "APIKeyが不正です"
              });
          }
      }
  }
  ```
* ApiKeyAuthorizeAttributeクラス
  * [TypeFilterAttribute](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute?view=aspnetcore-3.1)をオーバーライドし、コントローラーで使うAttributeと上記で作成した認可処理と紐づけます。
  * コンストラクタの中身は何も記述しない状態で大丈夫です。

  ```csharp
  using Microsoft.AspNetCore.Mvc;
  using Microsoft.AspNetCore.Mvc.Filters;
  using Microsoft.Extensions.Configuration;
  using System;
  using System.Linq;
  using System.Net;

  namespace SampleApi.Auth.Filters
  {
      public class ApiKeyAuthorizeAttribute : TypeFilterAttribute
      {
          public ApiKeyAuthorizeAttribute() : base(typeof(ApiKeyAuthorizeFilter))
          {
          }
      }
  }
  ```

* API Controllerクラス
  * B2Cのリクエストを受け付けるControllerクラスに上記で作成したAttributeを追加します。

  ```csharp
  [Route("api/[controller]")]
  [ApiController]
  [ApiKeyAuthorize]   // 追加
  public class AuthenticateController : ControllerBase
  {
      ...
  }
  ```

* Startup.cs
  * 認可処理を行うため`Startup.cs`の`Configure()`メソッドに`UseAuthentication`ミドルウェアを追加します。
  * 下記のコードの場合、追加する場所は`UseRouting`と`UseAuthorization`の間になります。
    * ミドルウェアの順番の詳細については[こちら](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0#middleware-order)をご参照ください。

  ```csharp
  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  {
      if (env.IsDevelopment())
      {
          app.UseDeveloperExceptionPage();
      }

      app.UseHttpsRedirection();

      app.UseRouting();

      app.UseAuthentication();  // 追加

      app.UseAuthorization();

      app.UseEndpoints(endpoints =>
      {
          endpoints.MapControllers();
      });
  }
  ```

* appsettings.json
  * 最後に、アプリケーション設定として「[ポリシーキーの作成](#ポリシーキーの作成)」で設定したAPIキーを追加します。
  * キー名は`ApiKeyAuthorizeFilter`クラスの`Configuration`に対応したキー名にします。

  ```json
  {
    "AuthorizedKey": "api-key-value"
  }
  ```

### 動作確認
REST APIを呼び出すB2Cのフローを動作確認してみます。
検証用に、リダイレクトURIは[公式のB2Cチュートリアル](https://docs.microsoft.com/ja-jp/azure/active-directory-b2c/tutorial-create-user-flows#test-the-user-flow)にも記載されている`https://jwt.ms`とします。
正しく認証が通ればトークンが表示されます。

* まず正常系を確認します。認証が完了し、トークンが返ってきました。
  * ※セキュリティの都合上ぼかしを入れていますが、実際にはトークンの文字列が続きます。
![B2C3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398865/9c29b5fd-702c-6466-8ac6-8644a74191b6.png)
* 次に異常系。REST API側のAPIキーを別の値に設定してみます。
  * 認証フローを実行すると、下記の通りエラーが返ってきました。ソースコードで記載したエラーメッセージも表示されています。
![B2C4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398865/32c4fc5d-22b9-1c32-516e-95a932fa5ba9.png)


### まとめ
* 今回はASP .NET Coreを使用したAzure AD B2C～REST API間のAPIキー認証をご紹介しました。
* REST APIの呼び出しについては、以前Qiitaに記事を投稿しております。そちらも合わせてご覧いただければと思います。
  * [Azure AD B2Cカスタムポリシーを利用し、認証フローの中で独自のREST APIを呼び出す](https://qiita.com/AsukaSeki/items/0d9cd890f1cd6ca91067)

## 関連記事
* [AD B2C記事まとめ](https://qiita.com/AsukaSeki/items/46afb6846cb4057b8050)