さて、前回は簡単に呼び出す奴を作ってみました。
今回は Azure AD 認証を付けたいと思います。
Azure AD にアプリの登録
では、Azure AD にアプリを登録します。サーバー側とクライアント側の 2 つを登録しましょう。
サーバーアプリの登録
とりあえずシングルテナント(自分のテナントのユーザーだけ)でさくっとサーバー側を作ります。
サーバー側のアプリが作成されたら、スコープを追加します。「APIの公開」で 「Scope の追加」を選択して適当な名前で作ります。 まず、アプリケーション ID URL を作るように言われるので「保存してから続ける」を選択します。
続けてスコープの追加です。適当に名前を入れて、同意できる人を管理者とユーザーにしてその他の項目も適当に埋めていきます。
スコープの追加ボタンを押すとスコープが追加されます。スコープの api://アプリID/スコープ名
は後で使うのでコピーしておきましょう。
クライアントアプリの登録
続けてクライアントのアプリを登録します。今回のクライアントアプリは .NET Core で作った WPF アプリです。 残念ながら各種ライブラリーが、まだ .NET Core には組み込みブラウザーなんてないと思って実装されてるので、そんな感じで登録します。
具体的にいうと、アプリ作成時(作成後でも編集できます)のリダイレクト URI で パブリッククライアント(モバイルとデスクトップ)を選択して http://localhost
を設定します。
アプリが出来たら「APIのアクセス許可」に行って「アクセス許可の追加」を選択します。API の選択画面になるので「自分の API」タブを選択して、先ほど作ったサーバー用アプリに作ったスコープを選択して「アクセス許可の追加」をします。
後で使うので、クライアントアプリの「概要」ページにいって「アプリケーション(クライアント)ID」をコピーしておきます。
テナント ID の取得
先ほどと同じアプリの「概要」ページから「ディレクトリ(テナント)ID」をコピーしておきます。
サーバーに認証機能を追加
では、サーバー側に認証機能を追加しましょう。 まず、以下の NuGet パッケージを入れます。
- Microsoft.AspNetCore.Authentication.JwtBearer 3.0.0-preview8.xxxxx
そして、Startup.cs
に JWT トークンによる認証の設定を追加します。
using GrpcService.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace GrpcServer { public class Startup { public void ConfigureServices(IServiceCollection services) { // これと services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "https://login.microsoftonline.com/ここにテナントID/"; options.Audience = "api://サーバー側アプリのクライアントID"; }); services.AddAuthorization(); services.AddGrpc(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); // これを追加 app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>(); endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); } } }
ここの ConfigureServices で先ほどメモした値を使います。Authority はテナントIDを埋め込んで、Audience にサーバー側アプリのスコープを作成したときに作られた api://アプリID
の値を設定します。先ほど控えたスコープの値から最後の /Call.API
をとった値になります。
そして gRPC のサービスに Authorize 属性をつけます。ここら辺は REST API と同じ感じですね。
using System.Threading.Tasks; using Grpc.Core; using GrpcSample; using Microsoft.AspNetCore.Authorization; namespace GrpcService.Services { [Authorize] public class GreeterService : Greeter.GreeterBase { public override Task<GreetReply> Greet(GreetRequest request, ServerCallContext context) { return Task.FromResult(new GreetReply { Message = $"Hello {request.Name}", }); } } }
この状態でアプリを起動して API を呼ぼうとすると、認証エラーになります。
因みに前回は Visual Studio Code で dotnet run
で起動したので別によかったのですが Visual Studio だとデフォルトで IIS Express で起動しようとするので、これをやめてやる必要があります。以下のような感じで設定変更できます。
クライアントにログイン機能を追加
では、クライアントにログイン機能を追加しましょう。MSAL.NET を追加します。パッケージ名は、Microsoft.Identity.Client
になります。
現時点での最新の 4.3.1 を入れました。
今回は簡単に実装するため全部 MainWindow.xaml.cs
に書いていきます。書き忘れたけどサーバー側も設定をハードコーディングしてるけど、ちゃんと設定から読むようにしてね。
using System; using System.Net.Http; using System.Linq; using System.Threading.Tasks; using System.Windows; using GrpcSample; using Microsoft.Identity.Client; namespace GrpcClient { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private string[] Scopes { get; } = new[] { "api://サーバーアプリのID/Call.API" }; // スコープ作った時にコピーしておいたやつ private readonly IPublicClientApplication _app; public MainWindow() { InitializeComponent(); // 本当は設定(appsettings.json とか)から読む var options = new PublicClientApplicationOptions { ClientId = "クライアントアプリのID", RedirectUri = "http://localhost", TenantId = "テナントのID", }; _app = PublicClientApplicationBuilder.CreateWithApplicationOptions(options).Build(); } private async void CallGrpcServiceButton_Click(object sender, RoutedEventArgs e) { var accessToken = await GetAccessTokenAsync(); using (var client = new HttpClient { BaseAddress = new Uri("https://localhost:5001") }) { var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client); var response = await greetServices.GreetAsync(new GreetRequest { Name = textBoxName.Text, }, new Grpc.Core.Metadata { { "Authorization", $"Bearer {accessToken}" }, }); MessageBox.Show(response.Message); } } private async Task<string> GetAccessTokenAsync() { AuthenticationResult r; try { var account = (await _app.GetAccountsAsync())?.FirstOrDefault(); r = await _app.AcquireTokenSilent(Scopes, account).ExecuteAsync(); } catch (MsalUiRequiredException) { r = await _app.AcquireTokenInteractive(Scopes) .WithSystemWebViewOptions(new SystemWebViewOptions { OpenBrowserAsync = SystemWebViewOptions.OpenWithChromeEdgeBrowserAsync, }) .ExecuteAsync(); } return r.AccessToken; } } }
IPublicClientApplication
の作成をしているコンストラクターの処理と、アクセストークンの取得をしている GetAccessTokenAsync
あたりが注目かな。
そして、最後に gRPC の API を呼び出すメソッドで Grpc.Core.Metadata
でお馴染みの Bearer でトークン渡してやる感じです。
では、実行してみましょう。
WPF のアプリでボタンを押すとブラウザーでログイン画面が開きます。アプリを作った Azure AD のテナントのユーザーでサインインしてください。 同意が求められるので逆らわずにいきましょう。
ログインに成功すると、ブラウザーは閉じていいよというメッセージが出ます。なので閉じて(ほっといてもいいけど)WPFアプリ側に戻ると gRPC の API が呼べてますね!やったね!
認証情報にアクセスしたい
サーバー側で認証情報触りたい場合はメソッドの引数の ServerCallContext
で GetHttpContext
を呼ぶと HttpContext が取れるので、それ経由でアクセスできます。例えば以下のような感じで
using System.Threading.Tasks; using Grpc.Core; using GrpcSample; using Microsoft.AspNetCore.Authorization; namespace GrpcService.Services { [Authorize] public class GreeterService : Greeter.GreeterBase { public override Task<GreetReply> Greet(GreetRequest request, ServerCallContext context) { var identity = context.GetHttpContext().User.Identity; return Task.FromResult(new GreetReply { Message = $"Hello {request.Name}, IsAuthorized: {identity.IsAuthenticated}, Your account is {identity.Name}", }); } } }
実行すると、以下のような感じになります。
まとめ
Azure App Service でホスト出来るようになるのを首を長くして待ってます。
ソースコードは前回のリポジトリに addauth というブランチを切ってあるので見てください。