Security Experiments with gRPC and ASP.NET Core 5

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

History

2020-11-25: Updated to .NET 5
2019-12-17: Updated Nuget packages, .NET Core 3.1, updated grpc implementations
2019-09-06: Updated Nuget packages, .NET Core 3 preview 9
2019-08-13: Updated Nuget packages, .NET Core 3 preview 8
2019-07-28: Updated Nuget packages, .NET Core 3 preview 7
2019-06-14: Updated Nuget packages, .NET Core 3 preview 6 changes
2019-05-07: Updated Nuget packages, .NET Core 3 preview 5 changes
2019-04-20: Updated Nuget packages, .NET Core 3 preview 4 changes
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.UseStaticFiles();
            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();
            app.UseCors();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>().RequireAuthorization("protectedScope");
                endpoints.MapGrpcService<DuplexService>().RequireAuthorization("protectedScope");
                endpoints.MapRazorPages();
            });
        }
    }
}

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.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Greet;
using Grpc.Core;
using Grpc.Net.Client;

namespace SecureGrpc.Client
{
    public class Program
    {
        static async Task Main(string[] args)
        {
            ///
            /// Token init
            /// 
            HttpClient httpClient = new HttpClient();
            ApiService apiService = new ApiService(httpClient);
            var token = await apiService.GetAccessTokenAsync();
            //var token = "This is invalid, I hope it fails";

            var tokenValue = "Bearer " + token;
            var metadata = new Metadata
            {
                { "Authorization", tokenValue }
            };

            var channel = GrpcChannel.ForAddress("https://localhost:50051", new GrpcChannelOptions
            {
                HttpClient = CreateHttpClient()
            });

            CallOptions callOptions = new CallOptions(metadata);

            var client = new Greeter.GreeterClient(channel);

            var reply = await client.SayHelloAsync(
                new HelloRequest { Name = "GreeterClient" }, callOptions);

            Console.WriteLine("Greeting: " + reply.Message);

            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }

        private static HttpClient CreateHttpClient()
        {
            var handler = new HttpClientHandler();
            var cert = new X509Certificate2(Path.Combine("Certs/client2.pfx"), "1111");
            handler.ClientCertificates.Add(cert);

            // Create client
            return new HttpClient(handler);
        }
    }
}

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://grpc.io/

An Early Look at gRPC and ASP.NET Core 3.0

https://www.zoeys.blog/first-impressions-of-grpc-integration-in-asp-net-core-3-preview/

10 comments

  1. […] Security Experiments with gRPC and ASP.NET Core 3.0 (Damien Bowden) […]

  2. […] Security Experiments with gRPC and ASP.NET Core 3.0 – Damien Bowden […]

  3. […] Security Experiments with gRPC and ASP.NET Core 3.0 (2019-03) […]

  4. Boris · · Reply

    Hey Damien,

    very nice and insightful article. I am currently working on a project, that utilizes grpc and I am trying to configure the grpc channels towards ssl. I used your example code with certificates generated by the answer of this article on SO (https://stackoverflow.com/questions/37714558/how-to-enable-server-side-ssl-for-grpc). I then generate the pfx via “openssl pkcs12 -export -passin -out server.pfx -inkey ca.key -in ca.crt -in server.crt”. But when a client tries to connect both client and server throw with a message that the remote certificate is invalid.

    Can you maybe share, how you created the certificates for you sample code? I am aware of this procedure not being production ready, but I need it for demo purposes.

    Thanks in advance

    1. Hi Boris, I just did it the way the docs did it. But it should work with standard certs, I have a sample here with the same cert problems, requirements

      Using Chained Certificates for Certificate Authentication in ASP.NET Core 3.1

      Will try this from scatch and document it, I expect it to be like the examples in the link

  5. Pavel Zika · · Reply

    Hi Boris, thanks a lot for interesting article.

    Have you any info about .NET 3.0 gRPC interoperability? I have tried to call simple server from DART client without success (similar sample for calling Google cloud api works well).

    Thanks

    1. pavel zika · · Reply

      Info: Problem solved – I used grpc ClientChanell with insecure connetion. It will (as default) use HTTP while my server provides HTTPS only.

  6. Awesome examples! Really thanks!!

  7. Eirik Mangseth · · Reply

    Damien, you wrote “The client then uses a cert to connect over HTTPS. This code will probably change before the release.” Has this changed since you wrote it? Is the client still required to use a cert to connect over https?

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.