Secure ASP.NET Core GRPC API hosted in a Linux kestrel Azure App Service

This article shows how to implement a secure GRPC API service implemented in ASP.NET Core and hosted on an Azure App Service using Linux and kestrel. An application Azure App registration is used to implement the security together with Microsoft.Identity.Web. A client credentials flow is used to acquire an application access token and the GRPC service validates the bearer token.

Code: https://github.com/damienbod/GrpcAzureAppServiceAppAuth

The service and the client use Azure AD to secure the service and request access tokens. The GRPC service is hosted on an Azure App service using kestrel and Linux. The service only allows HTTP 2.0. Any other identity provider could be used or if you are using a public client, then a delegated user access token would be used.

The following Nuget packages are used to implement the ASP.NET Core service:

  • Grpc.AspNetCore
  • Microsoft.Identity.Web

See the following docs for setting up an Azure App registration for a daemon app:

https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-daemon-overview

https://damienbod.com/2020/10/01/implement-azure-ad-client-credentials-flow-using-client-certificates-for-service-apis/

https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/2-Call-OwnApi

The services are setup to require a valid JWT access token to use the GRPC service and the GRPC API is also setup. It is important to only accept access tokens intended for this service. The ConfigureKestrel method is used to configure the port which must match the Azure App Service deployment. The HTTP20_ONLY_PORT value must match this. Only HTTP2 is used.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy =>
    {
        // Validate id of application for which the token was created
        // In this case the CC client application 
        validateAccessTokenPolicy.RequireClaim("azp", "b178f3a5-7588-492a-924f-72d7887b7e48");

        // only allow tokens which used "Private key JWT Client authentication"
        // // https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
        // Indicates how the client was authenticated. For a public client, the value is "0". 
        // If client ID and client secret are used, the value is "1". 
        // If a client certificate was used for authentication, the value is "2".
        validateAccessTokenPolicy.RequireClaim("azpacr", "1");
    });
});

builder.Services.AddGrpc();

// Configure Kestrel to listen on a specific HTTP port 
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(8080);
    // port must match the Azure App Service setup HTTP20_ONLY_PORT
    options.ListenAnyIP(7179, listenOptions =>
    {
        listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2;
    });
});

The middleware is setup like any other secure ASP.NET Core API application. The GRPC service is added to the endpoints.

var app = builder.Build();

app.UseHttpsRedirection();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGrpcService<GreeterService>();

    endpoints.MapGet("/", async context =>
    {
        await context.Response.WriteAsync("GRPC service running...");
    });
});

app.Run();

The GRPC service requires a proto3 definition. I reused the example file from the ASP.NET Core Microsoft documentation.

syntax = "proto3";

option csharp_namespace = "GrpcAzureAppServiceAppAuth";

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;
}

The GRPC API can then be protected using the Authorize attribute and the scheme and the authorization policy can be applied to the service.

using Grpc.Core;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;

namespace GrpcAzureAppServiceAppAuth;

[Authorize(Policy = "ValidateAccessTokenPolicy", 
AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}

Deploying the GRPC Azure App Service

Now that the ASP.NET Core GRPC serivce is setup, this can be deployed to an Azure App Service using Linux. See docs: use_gRPC_with_dotnet for full details. It is important to setup HTTP 2 and also to turn on the HTTP 2 proxy.

The HTTP20_ONLY_PORT configuration must be added with a value that matches the port in the code.

Implement a GRPC test client with Microsoft Identity client credentials flow for trusted clients

The following Nuget packages are used to implement the test GRPC client using an OAuth client credentials flow. This can only be used with an application which can keep a secret. You could also use certificates and implement client assertions to request the access token.

  • Microsoft.Identity.Client
  • Google.Protobuf
  • Grpc.Net.Client
  • Grpc.Tools

The application gets an access token, adds this to the GRPC client and requests data from the GRPC service. The base URL is read from the app.settings which match the Azure App Service deployment.

using Grpc.Net.Client;
using GrpcAzureAppServiceAppAuth;
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Client;
using Grpc.Core;

var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddUserSecrets("0464abbd-c57d-4048-873d-d16355586e50")
    .AddJsonFile("appsettings.json");

var configuration = builder.Build();

// 1. Client client credentials client
var app = ConfidentialClientApplicationBuilder
    .Create(configuration["AzureADServiceApi:ClientId"])
    .WithClientSecret(configuration["AzureADServiceApi:ClientSecret"])
    .WithAuthority(configuration["AzureADServiceApi:Authority"])
    .Build();

var scopes = new[] { configuration["AzureADServiceApi:Scope"] };

// 2. Get access token
var authResult = await app.AcquireTokenForClient(scopes)
    .ExecuteAsync();

if (authResult == null)
{
    Console.WriteLine("no auth result... ");
}
else
{
    Console.WriteLine(authResult.AccessToken);

    // 2. Use access token & service
    var tokenValue = "Bearer " + authResult.AccessToken;
    var metadata = new Metadata
    {
        { "Authorization", tokenValue }
    };

    var handler = new HttpClientHandler();

    var channel = GrpcChannel.ForAddress(
        configuration["AzureADServiceApi:ApiBaseAddress"], 
        new GrpcChannelOptions
    {
        HttpClient = new HttpClient(handler)
        
    });

    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();
}

Using GRPC together with Azure App Services is a really cool, easy to develop and now full duplex app services can be used from Azure App services with little effort. Kestrel with Linux is a state of the art hosting system.

Links

https://github.com/grpc/grpc-dotnet/

https://docs.microsoft.com/en-us/aspnet/core/grpc

https://github.com/Azure/app-service-linux-docs/blob/master/HowTo/gRPC/use_gRPC_with_dotnet.

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 )

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: