前回は 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
- Azure AD の設定
- Azure AD のテナントとユーザーの準備
- ASP.NET Core で Web API を作成
- 動作確認
- エラーでうまくいかないときは...
- 完成版のサンプルコード
- おわりに
- 余談: Azure AD 開発ウェビナーあるってよ♪
セットアップの方法を書いていきますが、最終的なサンプルのコードはこちらにおきました。
Azure AD の設定
まずは Azure AD をセットアップします。Web API で Authorization Code Flow with PKCE で認証を通過させたいので、id token だけじゃなく access token で認証を通るようにしておきましょう。
Azure AD のテナントとユーザーの準備
テナントがなくて新たに作りたい方はこちらのドキュメントをご参考に作成してみるサクッと作れると思います。
また、のちほどログインして検証するのでユーザーを追加したり招待したりして用意しておきましょう。
ユーザーは追加するとアカウントが一つ増えてパスワードの管理もひとつふえるので、既になんらかのアカウントがある場合は招待(=新しいゲスト ユーザーの追加)をした方がよいです。
アプリ登録
Azure Portal で Azure AD のテナントを開いて アプリ登録 → 新規登録 をクリックします。
以下を参考に値をセットして登録します。
- 名前 : 適当にわかりやすいものを入れます。ここでは
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/
で起動するのでこの値です。
登録すると、作成した app が開かれた画面になります。 認証 をクリックして設定内容を確認しておくと、プラットフォームが SPA で指定した Redirect URI が登録されていることと、Implicit flow が無効になっていることくらいでしょうか。
アプリ登録 > API の公開
access token で認証できるように API の公開をします。先ほど作成した app のメニューの API の公開 > Scope の追加 をクリックします。
初めて Scope を追加するときは アプリケーション ID の URI の登録画面がでます。値はデフォルトのままでよいです。保存してから続ける をクリックします。
スコープの追加の画面に遷移しますので、適当に値を入れて スコープの追加 をクリックして追加します。スコープ名はなんとなくで user_impersonation って名称にしました。サンプルの API を作るだけに深い意味はありません。
アプリ登録 > API のアクセス許可
今作成したスコープにアクセスできるようにします。API のアクセス許可 > アクセス許可の追加 をクリックします。
自分の API をクリックすると、自身が API を公開してる app の一覧が出てきます。今回いじってる swagger-auth-sample をクリックします。
アプリケーションに必要なアクセスの許可の種類は "委任されたアクセス許可" を選びます(もう一方は選択できませんがね)。
アクセス許可でさきほど作成した user_pmpersonation にチェックを入れて、アクセス許可の追加 をクリックして追加します。
アプリ登録 > マニフェスト
メニューの マニフェスト を開いてaccessTokenAcceptedVersion
を 2 にセットします。保存 ボタンは忘れずにクリックします。
この設定自体は必須ではないんですが、accessTokenAcceptedVersion
が null の場合( 1 として扱われる)と 2 の場合で Audience の設定方法が異なります。今回は今後を見据えて v2 になってる前提で設定方法を書いていくため、ここで設定をしておきました。
参考までにドキュメントはここら辺に書かれています。注意書きを読むと状況のカオス具合と 2 にしておこってお気持ちになります。
Azure AD の情報をメモ
以上で Azure AD の設定は完了です。あとは、ASP.NET Core の Web API に必要な情報をメモっておきましょう(Azure AD の画面を開いておけばよいだけですが共有だけしておきます)。
まずさきほど作成した app の概要を開きます。
Web API を構築する上で必要なのは ClientId と MetadataAddress の2つ。
- ClientId: 概要ページの アプリケーション(クライアント) ID の値
次は、エンドポイント をクリックします。
- 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://” から始まる値です。
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 アプリケーション を選んで次に進みます。
プロジェクト名とかは適当に入れて 作成 をクリックします。
後は下図のように選択します。
- ASP.NET Core 5.0 が選択できない場合は VS のバージョンが古いので更新しましょう。
- 5.0 だと Enable OpenAPI support のチェックを入れることで Swashbuckle の設定がされた状態になります。
- ASP.NET Core 3.1 で作成したい場合は、Swashbuckle の設定を自分でやる必要があります。前回のブログを参考に の "1. Swagger の導入設定(Swashbuckle)" と "2. デバッグ開始時に Swagger UI を起動する" に設定方法が書いてます。
appsettings.json の編集
ソリューションエクスプローラーで appsettings.json を開きます。
デフォルトで Logging
と AllowedHosts
がセットされています。今回は 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
をクリックします。
Execute をクリックすると実行した Curl のコマンドや Response が表示されます。Curl のコマンドでは Authorization ヘッダーがついてないことが確認できます。また、レスポンスは 401 が返ってきてるので、正常に動作していることが確認できます。
認証関連の動作確認は、まず認証できないことの確認が重要ですね。
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 ボタンをクリックしてみましょう。
軽く説明を加えるとしたら、`client_id
はデフォルトで設定してますのでいじらなくてよいです。client_secret
は空っぽのままで大丈夫です。
今回 AAD 側の platoform を SPA で設定してるので入力するとエラーになります。ちなみに SPA で設定しないと、token request 時に CORS のエラーで怒られます。
まぁ Authorization Code Flow での設定ミスあるあるなやつですね。
Scopes は両方にチェックをいれて、Authorize をクリックすると、認証処理が開始します。
リダイレクトされて Azure AD のサインイン画面が表示されます。サインインしましょう。
無事にサインインできるとこんな画面になります。
各 API の南京錠もロックされたアイコンに変わっています。get2 を実行してみましょう。
Curl のコマンドで Authorization ヘッダーにトークンを付与して API をコールしてることがわかります。また、Response の status code が 200 で値が取得できていることがわかります。
こんな感じで認証付きの API も楽に Swagger UI から検証できるようになります。
エラーでうまくいかないときは...
うまくいかないときはなんらかの設定が間違ってる可能性が高いのですがその時に私がやりそうな方法を紹介しておきます。
ブラウザの DevTools で確認
とりあえずさっと見れるのは、Chrome や Microsoft Edge だと F12
キーで起動する DevTools の Console でエラーを確認することです。下図だとエラーでてないですが(汗)。ここで CORS のエラー出てたら Azure AD の platoform が SPA 以外に設定してまってるやんとか切り分けることができます。何のエラーだとなにだってのを論理的にわかるようになるには多少の知識は必要ですが、とりあえず原因かもしれない可能性を見ることでできるかもしれない部分です。
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 やクエリパラメーターが正しいかとか...
token request のresponse body にエラーがはかれてることもあります(下図は正常な response bodyですが)。
ここまで client id とかにフィルターをかけてきたけど、最後に別にいいやってお気持ちになった
完成版のサンプルコード
ということで今回の完成版のコードは GitHub のこちらにおいてあります。
おわりに
個人的な経験から感じるのは、エラーを愚直に見るとエラー自体が見当違いのエラーのためハマることも多い認証系の開発ですが、認証フローをある程度理解することでエラーからどの問題やろかってのが理解できるようになるかなぁと感じています。
結局参考にしたのがここだけでしたのでちょっとハマりました。
余談: Azure AD 開発ウェビナーあるってよ♪
Azure AD の開発にこんなウェビナーがあります。Azure AD とは的なとこからなので5時間くらい?のがっつりしたのですが...まぁ全部見なくても、開発のところだけ見ていけば 1 ~ 2hとかな気がしますので、これから Azure AD 開発に入門したい方は観てみるとちょっとは参考になるかもしれません♪
基礎からまとまっていてすごい良かった。Implicit が非推奨って明言されてるのもよしhttps://t.co/dobWeUPJUy
— 82@育休中 (@watahani) January 15, 2021