DEV Community

loading...
The 425 Show

Secure gRPC service with .NET Core and Azure Active Directory

Christos Matskas
Program Manager in the Microsoft Identity Dev Advocacy team. Programmer, speaker and all around geek
Updated on ・6 min read

A few months back I was fortunate to record an excellent episode for the On .NET series on Channel 9 around gRPC and .NET Core. If you don't know what gRPC is or how it works in .NET, you can watch my video recording with Sourabh below:

Now that I work with the Microsoft Identity platform day in, day out, I was curious to see what it would take to secure a gRPC solution with the new Microsoft.Identity.Web on the server. Microsoft.Identity.Web is a new library for .NET Core (3.1 or later) that makes authentication and token management a breeze. The library is going to hit GA soon so now is a good time to experiment and see how to get our ASP.NET Core apps working with it.

Prerequisites

Create the Azure AD App registrations

gRPC services, although they act as APIs, can't be access by the browser. Therefore you need a client app that can be used to call our gRPC service. To be able to authenticate the user in the client app and validate the access tokens in the gRPC service, we will need 2 app registrations.

gRPC Service app registration

Head to the Azure AD portal and select the App registration tab. Click the New registration button and use the following settings to create the server app:

Alt Text

The Redirect URI assumes that we will be using the default port for Kestrel to run our gRPC service.

Our client app needs to know about the server app. Therefore, we need navigate to the Expose an API tab, set the App ID URI (leave the defaults) and then add a new scope:

Alt Text

Alt Text

That's all we need in our servcie app

gRPC client app registration

Back to the App Registrations tab, we need to create a new app registration. Select Single org and leave the Redirect URi blank. Once the app is created, head to the Authentication tab to Add a platform. Since our client app will be a console .NET Core app, we should choose the Mobile and desktop applications and use the https://..../nativeclient for the Redirect URI.

Alt Text

In the Advanced settings on the same tab, update the Default client type as per the image below:

Alt Text

Next, we need to add the API permissions for our gRPC service. Open the API Permissions tab and click the Add a permission. Select the My APIs and click on the GrpcService app (this could be different for you depending on the app name you used to register your service app).

Alt Text

Select the right permission and press Add permission

Alt Text

This is all we need to register the client app

Create the gRPC Service in .NET Core

We will be using the .NET CLI to create our gRPC service

dotnet new grpc

Alt Text

We now need to add a couple of NuGet packages for enabling authentication. Open the *.csproj file and add the following package reference

<PackageReference Include="Microsoft.Identity.Web" Version="0.2.3-preview" />

The gRPC service needs to know how to authenticate incoming access token so we have to update the appsettings.json file to include the grpc service app registration details as per the example below:

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "Domain": "cmatskas.onmicrosoft.com",
  "TenantId": "b55f0c51-0000-0000-0000-33569b247796",
  "ClientId": "e7ba8202-0000-0000-0000-90ee506bc5cf"
}

Next, we will edit the Startup.cs file to add the authentication code. Add a constructor to get access to the IConfiguration object.

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}
public IConfiguration Configuration { get; }

The authentication middleware goes into the ConfigureServices() method. Update it with the following code:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftWebApi(Configuration);
services.AddAuthorization();

Finally, let's not forget the Configure() method which needs to be update to include authN and authZ

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

The final step is to update the GreetService.cs class to ensure that any incoming requests are authenticated and have the right access token.

static string[] scopeRequiredByAPI = new string[] { "access_as_user" };

[Authorize]
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
    var httpContext = context.GetHttpContext();
    httpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByAPI);

    return Task.FromResult(new HelloReply
    {
         Message = "Hello " + request.Name
    });
}

The action above has an [Authorize] attribute and then we use the VerifyUserHasAnyAcceptedScope(<scopeArray>) method that is part of the new Microsoft.Identity.Web to ensure that the request has the appropriate scopes.

This is all we need to do to create an authenticate gRPC service. As you can see, the new Microsoft.Identity.Web provides a very powerful and simplified API to validate tokens.

Create the gRPC client (console) app

To keep things simple, we will create a .NET Core console app to speak to our .NET Core gRPC service. On a new directory, run

dotnet new console

We now need to add a number of NuGet packages, and a reference to our .proto file, which currently resides in our gRPC service directory.

NOTE: I've placed both projects (service and client) side by side so the path is relative to the location of my projects. This could be different for you

Open the *.csproj file and add the following xml

<ItemGroup>
   <PackageReference Include="Google.Protobuf" Version="3.13.0" />
   <PackageReference Include="Grpc.Net.ClientFactory" Version="2.31.0" />
   <PackageReference Include="Grpc.Tools" Version="2.31.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
   </PackageReference>
   <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="3.1.7" />
   <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
   <PackageReference Include="Microsoft.Identity.Client" Version="4.17.1" />
</ItemGroup>

<ItemGroup>
    <Protobuf Include="../grpcWithAuth/Protos/greet.proto" GrpcServices="Client" Link="Protos/greet.proto" />
</ItemGroup>

The first ItemGroup contains all the references we need to work with gRPC and the Microsoft.Identity.Client library that will be used to authenticate users.

We also need to add a new appsettings.json file where we can store the Azure AD settings for our client app, including the scopes we need for our gRPC service. The file should have the following json

{
    "AzureAd":{
        "ClientId": "28f7d0b6-d372-4cd9-9cbd-6fb48119536c",
        "TenantId": "b55f0c51-61a7-45c3-84df-33569b247796"
    },
    "scope": "api://e7ba8202-d92d-4b9b-be6b-90ee506bc5cf/access_as_user"
}

To keep our code clean, we'll add a new class DeviceCodeAuthProvider.cs to deal with the user authentication. The code need for that class is attached below:

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Client;

public class DeviceCodeAuthProvider
{
    private IPublicClientApplication msalClient;
    private IAccount userAccount;

    public DeviceCodeAuthProvider(IConfiguration config)
    {
        msalClient = PublicClientApplicationBuilder
            .Create(config["AzureAd:ClientId"])
            .WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
            .WithTenantId(config["AzureAd:TenantId"])
            .Build();
    }

    public async Task<string> GetAccessToken(string[] scopes)
    {
        if (userAccount == null)
        {
            try
            {
                var result = await msalClient.AcquireTokenWithDeviceCode(scopes, callback => {
                    Console.WriteLine(callback.Message);
                    return Task.FromResult(0);
                }).ExecuteAsync();

                userAccount = result.Account;
                return result.AccessToken;
            }
            catch (Exception exception)
            {
                Console.WriteLine($"Error getting access token: {exception.Message}");
                return null;
            }
        }
        else
        {
            var result = await msalClient
                .AcquireTokenSilent(scopes, userAccount)
                .ExecuteAsync();
            return result.AccessToken;
        }
    }
}

Finally, we need to update the Program.cs to autneticate the user and call the gRPC service:

using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using grpcWithAuth;
using Microsoft.Extensions.Configuration;

namespace grpcClient
{
    class Program
    {
        private static IConfiguration configuration;
        static async Task Main(string[] args)
        {
            LoadAppSettings();

            var authProvider = new DeviceCodeAuthProvider(configuration);
            var token = await authProvider.GetAccessToken(new string[] {configuration["scope"]});

            var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = new Greeter.GreeterClient(channel);

            var headers = new Metadata();
            headers.Add("Authorization", $"Bearer {token}");

            var request = new HelloRequest()
            {
                Name = "SpongeBob"
            };

            var reply = await client.SayHelloAsync(request, headers);
            Console.WriteLine(reply.Message);
        }

        static void LoadAppSettings()
        {
            configuration = new ConfigurationBuilder()
                            .SetBasePath(System.IO.Directory.GetCurrentDirectory())
                            .AddJsonFile("appsettings.json")
                            .Build();
        }
    }
}

Let's run both projects and see what happens :). First the gRPC service and then the client

Alt Text

Alt Text

Alt Text

After successfully authenticating, we can see the call to the gRPC service executing and the right result returned to our console app:

Alt Text

Sweet sauce!!! Everything is working as expected

Source code

You can get the full source code for this project on GitHub

Next up

Adding some MS Graph goodness to pull email and calendar information :)

Summary

This was a pretty straightforward setup. The authentication code was less than 8 lines for our gRPC service and we had to do a bit of work on the client app but nothing extravagant. All in all, the end-to-end implementation took little effort so I hope this shows how you can benefit from the .NET Core ecosystem to create powerful and fast gRPC services secured by Azure AD.

Discussion (0)