This article shows how a gRPC service could implement OAuth2 security using IdentityServer4 as the token service.
Code: https://github.com/damienbod/Secure_gRpc
Posts in this series
- Security Experiments with gRPC and ASP.NET Core 3.0
- Running Razor Pages and a gRPC service in a single ASP.NET Core application
History
2019-03-08: Removing the IHttpContextAccessor, no longer required
2019-03-07: Updated the auth security to configure this on the route, attributes are not supported in the current preview.
Setup
The application is implemented using 3 applications. A console application is used as the gRPC client. This application requests an access token for the gRPC server using the IdentityServer4 token service. The client application then sends the access token in the header of the HTTP2 request. The gRPC server then validates the token using Introspection, and if the token is valid, the data is returned. If the token is not valid, a RPC exception is created and sent back to the server.
At present, as this code is still in production, securing the API using the Authorization attributes with policies does not seem to work, so as a quick fix, the policy is added to the routing configuration.
The gRPC client and server were setup using the Visual Studio template for gRPC.
gRPC Server
The GreeterService class is the generated class from the Visual Studio template. The security bits were then added to this class. The Authorize attribute is added to the class which is how the security should work.
using System.Threading.Tasks; using Greet; using Grpc.Core; using Microsoft.AspNetCore.Authorization; namespace Secure_gRpc { [Authorize(Policy = "protectedScope")] public class GreeterService : Greeter.GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } } }
The startup class configures the gRPC service and the required security to use this service. IdentityServer4.AccessTokenValidation is used to validate the access token using introspection. The gRPC service is added along with the authorization and the authentication.
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using IdentityServer4.AccessTokenValidation; using Microsoft.AspNetCore.Authentication.JwtBearer; using System.Security.Claims; namespace Secure_gRpc { public class Startup { private string stsServer = "https://localhost:44352"; // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddAuthorization(options => { options.AddPolicy("protectedScope", policy => { policy.RequireClaim("scope", "grpc_protected_scope"); }); }); services.AddAuthorizationPolicyEvaluator(); services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication(options => { options.Authority = stsServer; options.ApiName = "ProtectedGrpc"; options.ApiSecret = "grpc_protected_secret"; options.RequireHttpsMetadata = false; }); services.AddGrpc(options => { options.EnableDetailedErrors = true; }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(routes => { routes.MapGrpcService<GreeterService>().RequireAuthorization("protectedScope"); }); app.UseAuthentication(); app.UseAuthorization(); } } }
The gRPC service is then setup to run using HTTPS and HTTP2.
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Hosting; namespace Secure_gRpc { 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>() .ConfigureKestrel(options => { options.Limits.MinRequestBodyDataRate = null; options.ListenLocalhost(50051, listenOptions => { listenOptions.UseHttps("server.pfx", "1111"); listenOptions.Protocols = HttpProtocols.Http2; }); }); }); } }
RPC interface definition
The RPC API is defined using proto3 and referenced in both projects. When the applications are built, the C# classes are created.
syntax = "proto3"; package Greet; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings. message HelloReply { string message = 1; }
gRPC client
The client is implemented in a simple console application. This client gets the access token from the IdentityServer4 token service, and adds it to the Authorization header as a bearer token. The client then uses a cert to connect over HTTPS. This code will probably change before the release. Then the API is called and the data is returned, or an exception. If you comment in the incorrect token, an auth exception is returned.
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Threading.Tasks; using Greet; using Grpc.Core; namespace Secure_gRpc { public class Program { static async Task Main(string[] args) { /// /// Token init /// HttpClient httpClient = new HttpClient(); ApiService apiService = new ApiService(httpClient); // switch the token here to use an invalid token, var token = await apiService.GetAccessTokenAsync(); //var token = "This is invalid, I hope it fails"; var tokenValue = "Bearer " + token; var metadata = new Metadata { { "Authorization", tokenValue } }; /// /// Call gRPC HTTPS /// var channelCredentials = new SslCredentials( File.ReadAllText("Certs\\ca.crt"), new KeyCertificatePair( File.ReadAllText("Certs\\client.crt"), File.ReadAllText("Certs\\client.key") ) ); CallOptions callOptions = new CallOptions(metadata); // Include port of the gRPC server as an application argument var port = args.Length > 0 ? args[0] : "50051"; var channel = new Channel("localhost:" + port, channelCredentials); var client = new Greeter.GreeterClient(channel); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }, callOptions); Console.WriteLine("Greeting: " + reply.Message); await channel.ShutdownAsync(); Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } } }
Sending a valid token
Sending an invalid token
This code is still in development, and a lot will change before the first release. The demo shows some of the new gRPC, HTTP2, hosting features which will be released as part of ASP.NET Core 3.0.
Links:
https://github.com/grpc/grpc-dotnet/
https://www.stevejgordon.co.uk/early-look-at-grpc-using-aspnet-core-3
https://www.zoeys.blog/first-impressions-of-grpc-integration-in-asp-net-core-3-preview/