はじめに
ASP.NET Core gRPC は ASP.NET Core のミドルウェアをサポートしているけど、 protobuf-net.Grpc を使ってコードファーストで実装する場合でも、 JWT Bearer 認証が使えるか試してみた。
Shared プロジェクトを作成
毎度おなじみの、 クライアントとサーバーの両方で使う、サービスコントラクトとデータコントラクトを定義する。 プロジェクトの種類は .NET Core クラスライブラリ。 ServiceContractAttribute を使うために System.ServiceModel.Primitives、 CallContext を使うために protobuf-net.Grpc のパッケージを追加しておく。
今回は JWT を取得するためのサービスコントラクトも定義している。
using System.Runtime.Serialization; using System.ServiceModel; using System.Threading.Tasks; using ProtoBuf.Grpc; namespace CodeFirstGrpc.Shared { [ServiceContract] public interface ITokenServie { ValueTask<TokenReply> TokenAsync( TokenRequest request); } [ServiceContract] public interface IGreetingService { ValueTask<HelloReply> HelloAsync(CallContext context = default); } [DataContract] public class TokenRequest { [DataMember(Order = 1)] public string UserName { get; set; } } [DataContract] public class TokenReply { [DataMember(Order = 1)] public string Token { get; set; } } [DataContract] public class HelloReply { [DataMember(Order = 1)] public string Message { get; set; } } }
Server プロジェクトを作成
gRPC サービスプロジェクトを作成し、コードファーストで gRPC サービスを実装する。 コードファーストでやるために、 protobuf-net.Grpc.AspNetCore パッケージを追加しておく。
Microsoft.AspNetCore.Authentication.JwtBearer パッケージも追加し、 JWT Bearer 認証を組み込む。
using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Threading.Tasks; using CodeFirstGrpc.Shared; using Grpc.Core; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using ProtoBuf.Grpc; using ProtoBuf.Grpc.Server; namespace CodeFirstGrpc.Server { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddCodeFirstGrpc(); services.AddAuthorization(options => { // デフォルトの認証スキーマのポリシーを簡単なものに上書き options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy => { policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.RequireClaim(ClaimTypes.Name); }); }); // JWT Bearer 認証を組み込む services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { // サンプルを簡略化するため検証機能を OFF にする // 本番でこんなことをしてはダメ options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidateIssuer = false, ValidateActor = false, ValidateLifetime = true, IssuerSigningKey = SecurityKey }; }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); // パイプラインに認証と認可を組み込む app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<TokenService>(); endpoints.MapGrpcService<GreetingService>(); }); } internal static readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); } public class TokenService : ITokenServie { public ValueTask<TokenReply> TokenAsync(TokenRequest request) { var token = GenerateJwtToken(request.UserName); return new ValueTask<TokenReply>(new TokenReply { Token = token, }); } string GenerateJwtToken(string name) { if (string.IsNullOrEmpty(name)) { throw new InvalidOperationException("Name is not specified."); } var claims = new[] { new Claim(ClaimTypes.Name, name), }; var credentials = new SigningCredentials( Startup.SecurityKey, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( "ExampleServer", "ExampleClients", claims, expires: DateTime.Now.AddSeconds(60), signingCredentials: credentials); return JwtTokenHandler.WriteToken(token); } readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); } // 認証必須のサービス [Authorize] public class GreetingService : IGreetingService { public ValueTask<HelloReply> HelloAsync(CallContext context = default) { // 認証に成功していれば HttpContext からユーザーの情報を取得できるはず var httpContext = context.ServerCallContext.GetHttpContext(); var userName = httpContext.User.Identity.Name; return new ValueTask<HelloReply>(new HelloReply { Message = $"Hello, {userName}.", }); } } }
Client プロジェクトを作成
.NET Core コンソールアプリのプロジェクトを作成し、 gRPC サービスを呼び出すためのクライアントを実装する。 コードファーストで実装した gRPC サービスを呼び出すために、 Grpc.Net.Client と protobuf-net.Grpc のパッケージを参照しておく。
using System; using System.Threading.Tasks; using CodeFirstGrpc.Shared; using Grpc.Core; using Grpc.Net.Client; using ProtoBuf.Grpc.Client; namespace CodeFirstGrpc.Client { class Program { static async Task Main(string[] args) { var channel = GrpcChannel.ForAddress("https://localhost:5001"); // JWT Bearer トークン無しで保護されたサービスを呼び出す var greetingClient = channel.CreateGrpcService<IGreetingService>(); try { _ = await greetingClient.HelloAsync(); } catch (RpcException ex) { Console.WriteLine(ex.Message); } // JWT Bearer トークン取得 var tokenClient = channel.CreateGrpcService<ITokenServie>(); var tokenReply = await tokenClient.TokenAsync(new TokenRequest { UserName = "Kubo", }); // JWT Bearer トークンをヘッダーに付与して、保護されたサービスを呼び出す // CallOptions は CallContext に暗黙的に変換される var callOptions = new CallOptions(headers: new Metadata { new Metadata.Entry("Authorization", $"Bearer {tokenReply.Token}"), }); var helloRepoly = await greetingClient.HelloAsync(callOptions); Console.WriteLine($"Message: {helloRepoly.Message}"); Console.ReadLine(); } } }
Authorization ヘッダーは、Metadata に追加すれば良いみたいだった。
実行結果
Authorization ヘッダー無しで認証必須の gRPC サービスを呼び出すと、ちゃんとエラーになった。 Authorization ヘッダーを設定した場合は呼び出し成功。
おわりに
コードファーストで ASP.NET Core gRPC のサービスを実装した場合でも、 ASP.NET Core の認証・認可ミドルウェアが使えそうだ。 gRPC サービスの認証をどうするかは課題だったので、ASP.NET Core MVC で慣れ親しんだ方法が使えるのは大きい。