BEACHSIDE BLOG

Azure とか C# 好きなエンジニアの個人メモ ( ・ㅂ・)و ̑̑

Swagger UI で Azure AD の認証をする (ASP.NET Core, Authorization Code Flow with PKCE)

前回は Open API の基本的な設定をしましたが、今回のゴールはこんな感じ。

  • Swagger UI の Authorize ボタンから Azure Active Directory (Azure AD) のサインイン画面にとんで、サインインできたらトークンを取得する
  • Swagger UI で認証が必要な API を Authorization ヘッダー付きでコールできるようにする
  • Web API は ASP.NET Core 5.0 (3.1 でも設定はほぼ一緒、一部の NuGet package のバージョンが異なるくらい)
  • IdP は Azure Active Directory で、認証フローは Authorization Code Flow with PKCE

セットアップの方法を書いていきますが、最終的なサンプルのコードはこちらにおきました。

https://github.com/beachside-project/aspnet-core-openapi-sandbox/tree/main/src/AspnetCore50WithAzureAdAuthorizationCode

Azure AD の設定

まずは Azure AD をセットアップします。Web API で Authorization Code Flow with PKCE で認証を通過させたいので、id token だけじゃなく access token で認証を通るようにしておきましょう。

Azure AD のテナントとユーザーの準備

テナントがなくて新たに作りたい方はこちらのドキュメントをご参考に作成してみるサクッと作れると思います。

また、のちほどログインして検証するのでユーザーを追加したり招待したりして用意しておきましょう。

ユーザーは追加するとアカウントが一つ増えてパスワードの管理もひとつふえるので、既になんらかのアカウントがある場合は招待(=新しいゲスト ユーザーの追加)をした方がよいです。

アプリ登録

Azure Portal で Azure AD のテナントを開いて アプリ登録新規登録 をクリックします。

f:id:beachside:20210122152900p:plain

以下を参考に値をセットして登録します。

  • 名前 : 適当にわかりやすいものを入れます。ここでは swagger-auth-sample といういまいちな名前にしました。
  • サポートされているアカウントの種類: シングルテナントにするかマルチテナントかにするかってやつです。今回はなんでもよいですが、シングルテナントの設定にしました。
  • リダイレクト URI: ここは間違えると死ぬ重要なポイントです。Swagger UI へのリダイレクトを設定します。プラットフォームは シングルページアプリケーション (SPA)を選びます。で URI はここでは https://localhost:5001/swagger/oauth2-redirect.html にしています。まずは Web API でデバッグ用をセットしてます。base url + swagger/oauth2-redirect.html の値です。私の環境だと ASP.NET Core でデバッグする際は https://localhost:5001/ で起動するのでこの値です。

f:id:beachside:20210122153043p:plain

登録すると、作成した app が開かれた画面になります。 認証 をクリックして設定内容を確認しておくと、プラットフォームが SPA で指定した Redirect URI が登録されていることと、Implicit flow が無効になっていることくらいでしょうか。

f:id:beachside:20210122153755p:plain

アプリ登録 > API の公開

access token で認証できるように API の公開をします。先ほど作成した app のメニューの API の公開 > Scope の追加 をクリックします。

初めて Scope を追加するときは アプリケーション ID の URI の登録画面がでます。値はデフォルトのままでよいです。保存してから続ける をクリックします。

f:id:beachside:20210122154150p:plain

スコープの追加の画面に遷移しますので、適当に値を入れて スコープの追加 をクリックして追加します。スコープ名はなんとなくで user_impersonation って名称にしました。サンプルの API を作るだけに深い意味はありません。

f:id:beachside:20210122154806p:plain

アプリ登録 > API のアクセス許可

今作成したスコープにアクセスできるようにします。API のアクセス許可 > アクセス許可の追加 をクリックします。

f:id:beachside:20210122154904p:plain

自分の API をクリックすると、自身が API を公開してる app の一覧が出てきます。今回いじってる swagger-auth-sample をクリックします。

f:id:beachside:20210122155039p:plain

アプリケーションに必要なアクセスの許可の種類は "委任されたアクセス許可" を選びます(もう一方は選択できませんがね)。
アクセス許可でさきほど作成した user_pmpersonation にチェックを入れて、アクセス許可の追加 をクリックして追加します。

f:id:beachside:20210122155255p:plain

アプリ登録 > マニフェスト

メニューの マニフェスト を開いてaccessTokenAcceptedVersion2 にセットします。保存 ボタンは忘れずにクリックします。

f:id:beachside:20210122164058p:plain

この設定自体は必須ではないんですが、accessTokenAcceptedVersion が null の場合( 1 として扱われる)と 2 の場合で Audience の設定方法が異なります。今回は今後を見据えて v2 になってる前提で設定方法を書いていくため、ここで設定をしておきました。

参考までにドキュメントはここら辺に書かれています。注意書きを読むと状況のカオス具合と 2 にしておこってお気持ちになります。

Azure AD の情報をメモ

以上で Azure AD の設定は完了です。あとは、ASP.NET Core の Web API に必要な情報をメモっておきましょう(Azure AD の画面を開いておけばよいだけですが共有だけしておきます)。

まずさきほど作成した app の概要を開きます。

f:id:beachside:20210122161821p:plain

Web API を構築する上で必要なのは ClientId と MetadataAddress の2つ。

  • ClientId: 概要ページの アプリケーション(クライアント) ID の値

次は、エンドポイント をクリックします。 f:id:beachside:20210122162244p:plain

  • MetadataAddress: OpenID Connect メタデータ ドキュメント の値

Swagger UI を構築する上で必要なのは "Authorization endpoint" 、"Token endpoint" 、"Scope の full name"3つ。

  • AuthorizationUrl: "OAuth 2.0 承認エンドポイント (v2)"の値
  • TokenUrl: "OAuth 2.0 トークン エンドポイント (v2)" の値

  • Scope の full name: メニューの API のアクセス許可 をクリックして先ほど登録した user_impersonation をクリックすると表示される "api://” から始まる値です。

f:id:beachside:20210122162716p:plain

ASP.NET Core で Web API を作成

ここからは、ASP.NET Core 5.0 の Web API を作成していきます。

ちなみに Swashbuckle.AspNetCore のバージョンは現時点で最新の v5.6.3 を使っています。

Web API プロジェクトの作成

Visual Studio 2019 で新しいプロジェクトを作成します。プロジェクトテンプレートは、C# の ASP.NET Core Web アプリケーション を選んで次に進みます。

f:id:beachside:20210122165243p:plain

プロジェクト名とかは適当に入れて 作成 をクリックします。

f:id:beachside:20210122165428p:plain

後は下図のように選択します。

  • ASP.NET Core 5.0 が選択できない場合は VS のバージョンが古いので更新しましょう。
  • 5.0 だと Enable OpenAPI support のチェックを入れることで Swashbuckle の設定がされた状態になります。
  • ASP.NET Core 3.1 で作成したい場合は、Swashbuckle の設定を自分でやる必要があります。前回のブログを参考に の "1. Swagger の導入設定(Swashbuckle)" と "2. デバッグ開始時に Swagger UI を起動する" に設定方法が書いてます。

f:id:beachside:20210113163827p:plain

appsettings.json の編集

ソリューションエクスプローラーで appsettings.json を開きます。

f:id:beachside:20210122170140p:plain

デフォルトで LoggingAllowedHosts がセットされています。今回は AzureAd のオブジェクト定義してその中に Azure AD の先ほどめもった情報をセットします。"apiScope" には前述の "Scope の full name" を入れておきます。スコープについてはガチだと複数になる場合もあるので配列にした方がよいとかありますが今回はシンプルに文字列で定義してます。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AzureAd": {
    "clientId": "",
    "metadataAddress": "",
    "AuthorizationUrl": "",
    "tokenUrl": "",
    "apiScope": ""
  },
  "AllowedHosts": "*"
}

Startup.cs で認証・認可を設定

Startup.cs を開いて ConfigureServices メソッドで認証のコードを追加します。

ASP.NET Core 3.1 の場合、NuGet の Microsoft.AspNetCore.Authentication.JwtBearer をインストールします。5.0 だとデフォルトでインストールされています。

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
        {
            options.MetadataAddress = Configuration.GetValue<string>("AzureAd:MetadataAddress");
            options.Audience = Configuration.GetValue<string>("AzureAd:clientId");
        });
//以下省略

Web API の認証の設定は最低限だとこれだけで実現できます。ケース次第で Token の validation のカスタマイズとか、Issuer の validate とか設定する必要もあるかとは思いますが、今回はシンプルに最低限の実装。

次に Configure メソッドの app.UseAuthorization(); の上に app.UseAuthentication(); を追加します。

// 略
  app.UseAuthentication();  // これを追加
  app.UseAuthorization();
// 略

Controller のカスタマイズ

雑に認証が必要な API を追加しておきましょう。デフォルトで作成される WeatherForecastController.cs を開き、以下のメソッドを足しておきましょう (using ステートメントはよしなに足してください)。

[Authorize]
[HttpGet]
[Route("get2")]
[Produces("application/json")]
public IEnumerable<WeatherForecast> Get2()
{
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
        .ToArray();
}

デバッグ実行

認証で保護されてアクセスできないことを確認するために、デバッグ実行してみましょう。デバッグを開始すると Swagger の画面が立ち上がります。

デバッグ実行で base url が https://localhost:5001/ ではない場合、前述で設定した Azure AD の app の認証でリダイレクト URI を正しい値に変えるか、Visual Studio でデバッグの URL (ポートだけだと思いますが) を変えて、この二つの値が同一になるように合わせる必要があります。

Try id out をクリックします。

f:id:beachside:20210122172421p:plain

Execute をクリックすると実行した Curl のコマンドや Response が表示されます。Curl のコマンドでは Authorization ヘッダーがついてないことが確認できます。また、レスポンスは 401 が返ってきてるので、正常に動作していることが確認できます。

認証関連の動作確認は、まず認証できないことの確認が重要ですね。

f:id:beachside:20210122173255p:plain

Swagger で認証の設定

ここから Swagger UI の Authorize ボタンをクリックすることで認証してトークンを取得できるように、2か所の設定をします。

Startup.cs を開きます。

まず1つめは ConfigureServices にある services.AddSwaggerGen(c => の部分を以下のようにします。

もうただの設定なのでこうするだけやって感じではあるんですが、すべての API でこの認証を使うように c.AddSecurityRequirement の部分を書いています。

あとは、Scopes には必要なスコープを入れましょうってくらいでしょうか。"openid" はこのブログの文面だけだと別に使いませんが、予期せぬトラブルシュートとかの時に id token も検証には役立つこともあるので、個人的にはいつも入れてます。

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "AspnetCore50WithAzureAdAuthorizationCode", Version = "v1" });

    c.AddSecurityDefinition("Azure AD - Authorization Code Flow", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.OAuth2,
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri(Configuration.GetValue<string>("AzureAd:AuthorizationUrl")),
                TokenUrl = new Uri(Configuration.GetValue<string>("AzureAd:tokenUrl")),
                Scopes = new Dictionary<string, string>
                {
                    ["openid"] = "Sign In Permissions",
                    [Configuration.GetValue<string>("AzureAd:apiScope")] = "API permission"
                },
            }
        },
        Description = "Azure AD Authorization Code Flow authorization",
        In = ParameterLocation.Header,
        Name = "Authorization",
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id ="Azure AD - Authorization Code Flow"},
            },
            // Scope は必要に応じて入力する
            new string [] {}
        },
    });
});

2つめは、Configure メソッドの中の app.UseSwaggerUI(c => の部分を以下のように変えます。

ここもポイントは特にないんですが、このメソッド自体が、if (env.IsDevelopment()) の中にあるので、別の環境でも Swagger を表示させたい場合は if の条件をカスタマイズする必要があるくらいでしょうか。

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "SwaggerSandboxAspnetCore31WithAzureAd v1");
    c.OAuthClientId(Configuration.GetValue<string>("AzureAd:clientId"));
    c.OAuthUsePkce();
});

動作確認

デバッグ実行してみましょう。ブラウザーが起動すると思いますが、認証が絡む場合の動作確認はシークレットウインドウ的なのを起動して swagger UI を表示した方がよいです。理由はあるあるな話で cookie や session storage とかの情報を共有されると認証の確認がしにくくなる場合があるからですね。

私の場合、下図は律儀(?)に Microsoft Edge の InPrivate ウインドウでやってます (たまたまこれやってる時にクライアント側の検証も合わせてやっててそっちに Chrome のシークレットウインドウを使ってるだけです)

Authorize のボタンができてます。また 各 API に南京錠のロックが空いたアイコンがついてます。c.AddSecurityRequirement でこれらどれをクリックしても同じ認証を使うように設定してますので、とりあえず Authorize ボタンをクリックしてみましょう。

f:id:beachside:20210122175503p:plain

軽く説明を加えるとしたら、`client_id はデフォルトで設定してますのでいじらなくてよいです。client_secret は空っぽのままで大丈夫です。
今回 AAD 側の platoform を SPA で設定してるので入力するとエラーになります。ちなみに SPA で設定しないと、token request 時に CORS のエラーで怒られます。
まぁ Authorization Code Flow での設定ミスあるあるなやつですね。

Scopes は両方にチェックをいれて、Authorize をクリックすると、認証処理が開始します。

f:id:beachside:20210122180532p:plain

リダイレクトされて Azure AD のサインイン画面が表示されます。サインインしましょう。

f:id:beachside:20210122181537p:plain

無事にサインインできるとこんな画面になります。

f:id:beachside:20210122181441p:plain

各 API の南京錠もロックされたアイコンに変わっています。get2 を実行してみましょう。

Curl のコマンドで Authorization ヘッダーにトークンを付与して API をコールしてることがわかります。また、Response の status code が 200 で値が取得できていることがわかります。

f:id:beachside:20210122181737p:plain

こんな感じで認証付きの API も楽に Swagger UI から検証できるようになります。

エラーでうまくいかないときは...

うまくいかないときはなんらかの設定が間違ってる可能性が高いのですがその時に私がやりそうな方法を紹介しておきます。

ブラウザの DevTools で確認

とりあえずさっと見れるのは、Chrome や Microsoft Edge だと F12 キーで起動する DevTools の Console でエラーを確認することです。下図だとエラーでてないですが(汗)。ここで CORS のエラー出てたら Azure AD の platoform が SPA 以外に設定してまってるやんとか切り分けることができます。何のエラーだとなにだってのを論理的にわかるようになるには多少の知識は必要ですが、とりあえず原因かもしれない可能性を見ることでできるかもしれない部分です。

f:id:beachside:20210123024240p:plain

OnAuthenticationFailed event をキャプチャする

OnAuthenticationFailed をキャプチャすることでエラーが見やすくなることがあります。初歩的なミスの場合はここでわかるかなーってのが私の個人的な印象。

まず、Startup.cs を開きましょう。

このメソッドを追加します。このコードのエラーメッセージの表示だけでも十分ですし、ブレークポイントも貼っておいて AuthenticationFailedContext の中身を見るってのがわかりやすいです。

private static async Task AuthenticationFailed(AuthenticationFailedContext arg)
{
    var message = $"AuthenticationFailed: {arg.Exception.Message}";
    arg.Response.ContentLength = message.Length;
    arg.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
    await arg.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(message), 0, message.Length);
}

あとは、.AddJwtBearer 拡張メソッドでこんな感じにセットします。

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.MetadataAddress = Configuration.GetValue<string>("AzureAd:MetadataAddress");
        options.Audience = Configuration.GetValue<string>("AzureAd:clientId");
        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = AuthenticationFailed
        };
    });

ある程度こなれてくるとここではエラーがヒットしなくなります。

ネットワークをキャプチャして認証のリクエストを確認

Authorization Code Flow など認証は画面では見えないところで request を送信してます。フリーのツール(Fiddler 4 とか)を使ってネットワークをキャプチャして、リクエストとそのレスポンスやエラーをみることで原因を突き止めやすくなる場合もあります。

Fiddler 4 だとダウンロードリンクはここでいいのかな (最近新たに DL してないからわからので自己責任でお願いします) Download Fiddler Classic

よくみるのは、authorization request の url やクエリパラメーターが正しいかとか...

f:id:beachside:20210122182850p:plain

token request のresponse body にエラーがはかれてることもあります(下図は正常な response bodyですが)。

f:id:beachside:20210122183000p:plain

ここまで client id とかにフィルターをかけてきたけど、最後に別にいいやってお気持ちになった

完成版のサンプルコード

ということで今回の完成版のコードは GitHub のこちらにおいてあります。

https://github.com/beachside-project/aspnet-core-openapi-sandbox/tree/main/src/AspnetCore50WithAzureAdAuthorizationCode

おわりに

個人的な経験から感じるのは、エラーを愚直に見るとエラー自体が見当違いのエラーのためハマることも多い認証系の開発ですが、認証フローをある程度理解することでエラーからどの問題やろかってのが理解できるようになるかなぁと感じています。

結局参考にしたのがここだけでしたのでちょっとハマりました。

GitHub - domaindrivendev/Swashbuckle.AspNetCore: Swagger tools for documenting API's built on ASP.NET Core

余談: Azure AD 開発ウェビナーあるってよ♪

Azure AD の開発にこんなウェビナーがあります。Azure AD とは的なとこからなので5時間くらい?のがっつりしたのですが...まぁ全部見なくても、開発のところだけ見ていけば 1 ~ 2hとかな気がしますので、これから Azure AD 開発に入門したい方は観てみるとちょっとは参考になるかもしれません♪