Quantcast
Channel: かずきのBlog@hatena
Viewing all articles
Browse latest Browse all 1387

ASP.NET Core 3.0 Preview 8 で gRPC に Azure AD 認証つけてみよう

$
0
0

さて、前回は簡単に呼び出す奴を作ってみました。

blog.okazuki.jp

今回は Azure AD 認証を付けたいと思います。

Azure AD にアプリの登録

では、Azure AD にアプリを登録します。サーバー側とクライアント側の 2 つを登録しましょう。

サーバーアプリの登録

とりあえずシングルテナント(自分のテナントのユーザーだけ)でさくっとサーバー側を作ります。

f:id:okazuki:20190903171712p:plain

サーバー側のアプリが作成されたら、スコープを追加します。「APIの公開」で 「Scope の追加」を選択して適当な名前で作ります。 まず、アプリケーション ID URL を作るように言われるので「保存してから続ける」を選択します。

f:id:okazuki:20190903171920p:plain

続けてスコープの追加です。適当に名前を入れて、同意できる人を管理者とユーザーにしてその他の項目も適当に埋めていきます。

f:id:okazuki:20190903172116p:plain

スコープの追加ボタンを押すとスコープが追加されます。スコープの api://アプリID/スコープ名は後で使うのでコピーしておきましょう。

クライアントアプリの登録

続けてクライアントのアプリを登録します。今回のクライアントアプリは .NET Core で作った WPF アプリです。 残念ながら各種ライブラリーが、まだ .NET Core には組み込みブラウザーなんてないと思って実装されてるので、そんな感じで登録します。

具体的にいうと、アプリ作成時(作成後でも編集できます)のリダイレクト URI で パブリッククライアント(モバイルとデスクトップ)を選択して http://localhostを設定します。

f:id:okazuki:20190903172444p:plain

アプリが出来たら「APIのアクセス許可」に行って「アクセス許可の追加」を選択します。API の選択画面になるので「自分の API」タブを選択して、先ほど作ったサーバー用アプリに作ったスコープを選択して「アクセス許可の追加」をします。

f:id:okazuki:20190903172809p:plain

後で使うので、クライアントアプリの「概要」ページにいって「アプリケーション(クライアント)ID」をコピーしておきます。

f:id:okazuki:20190903173043p:plain

テナント 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
{
    publicclass Startup
    {
        publicvoid ConfigureServices(IServiceCollection services)
        {
            // これと
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.Authority = "https://login.microsoftonline.com/ここにテナントID/";
                    options.Audience = "api://サーバー側アプリのクライアントID";
                });
            services.AddAuthorization();

            services.AddGrpc();
        }

        publicvoid 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]
    publicclass GreeterService : Greeter.GreeterBase
    {
        publicoverride Task<GreetReply> Greet(GreetRequest request, ServerCallContext context)
        {
            return Task.FromResult(new GreetReply
            {
                Message = $"Hello {request.Name}",
            });
        }
    }
}

この状態でアプリを起動して API を呼ぼうとすると、認証エラーになります。

f:id:okazuki:20190903174408p:plain

因みに前回は Visual Studio Code で dotnet runで起動したので別によかったのですが Visual Studio だとデフォルトで IIS Express で起動しようとするので、これをやめてやる必要があります。以下のような感じで設定変更できます。

f:id:okazuki:20190903174134p:plain

クライアントにログイン機能を追加

では、クライアントにログイン機能を追加しましょう。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>publicpartialclass MainWindow : Window
    {
        privatestring[] Scopes { get; } = new[] { "api://サーバーアプリのID/Call.API" }; // スコープ作った時にコピーしておいたやつprivatereadonly 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 が呼べてますね!やったね!

f:id:okazuki:20190903175948p:plain

認証情報にアクセスしたい

サーバー側で認証情報触りたい場合はメソッドの引数の ServerCallContextGetHttpContextを呼ぶと HttpContext が取れるので、それ経由でアクセスできます。例えば以下のような感じで

using System.Threading.Tasks;
using Grpc.Core;
using GrpcSample;
using Microsoft.AspNetCore.Authorization;

namespace GrpcService.Services
{
    [Authorize]
    publicclass GreeterService : Greeter.GreeterBase
    {
        publicoverride 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}",
            });
        }
    }
}

実行すると、以下のような感じになります。

f:id:okazuki:20190903180359p:plain

まとめ

Azure App Service でホスト出来るようになるのを首を長くして待ってます。

ソースコードは前回のリポジトリに addauth というブランチを切ってあるので見てください。

github.com


Viewing all articles
Browse latest Browse all 1387

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>