Security Experiments with gRPC and ASP.NET Core 3.0

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

2019-06-14: Updated Nuget packages, .NET Core 3 preview6 changes
2019-05-07: Updated Nuget packages, .NET Core 3 preview5 changes
2019-04-20: Updated Nuget packages, .NET Core 3 preview4 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.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://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/

5 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

      https://damienbod.com/2019/06/27/using-chained-certificates-for-certificate-authentication-in-asp-net-core-3-0/

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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

%d bloggers like this: