Securely accessing MS Graph from gRPC .NET with Azure AD
Tags: azure, security
Published: 2020-10-09 20:11:41

A few weeks ago (time flies by!!) I posted a blog post about securing .NET gRPC services using the Microsoft Identity platform, a.k.a Azure Active Directory. You can read the blog post [here](https://dev.to/425show/secure-grpc-service-with-net-core-and-azure-active-directory-4p6k).

Today, I decided to take this one step further and extend the service to pull data from Microsoft Graph. The updated project comes with a slew of changes as many things changed since the last blog post:

1. [Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web), the authentication and token management library hit GA and there have been some API changes.
2. The API changes include tight integration with Microsoft Graph which is now supported out of the box with a new NuGet package.

You can read all about these changes on our [GA announcement blog post](https://developer.microsoft.com/en-us/identity/blogs/microsoft-identity-web-is-now-generally-available/).

And if you're wondering why should you use gRPC over normal APIs that parse JSON, then this tweet from James Netwon-King (the author of Newtonsoft.json) should help put things into perspective:

{% twitter 1310875638585204738 %}

## Let's write some code!
Enough talking, let's get to coding. Open the previous gRPC project (clone from [here](https://github.com/cmatskas/gRPCAuthWithAzureAD)) and let's get cracking. First, in our `grpcWithAuth` project we need to update the NuGet packages. I like to mess with my `*.csproj` files so this is what my XML looks like after updating the packages to the latest version:

``` 
<ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.27.0" />
    <PackageReference Include="Microsoft.Identity.Web" Version="1.1.0" />
    <PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.1.0" />
</ItemGroup>
```
Since we will be interacting with the MS Graph, we need to create a new proto file to define the service and messages. In the **Protos** folder, add a new file `graph.proto` and paste the following content:

```
syntax = "proto3";

option csharp_namespace = "grpcWithAuth";

package graph;

service GraphService {
    // Sends the calendar info from Graph
    rpc GetCalendar (CalendarRequest) returns (CalendarReply);
  }
  
  // The request message
  message CalendarRequest {
    string name = 1;
  }
  
  // The response message containing the calendar information.
  message CalendarReply {
    string subject = 1;
    string bodyPreview = 2;
    string start = 3;
    string end = 4;
  }
```
Next, we need to add a new service to handle the incoming requests. In the **Services** folder add a new `GraphAPIService.cs` class. The code for this class is provided below:

```
using System;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Microsoft.Identity.Web.Resource;

namespace grpcWithAuth
{
    public class GraphAPIService : GraphService.GraphServiceBase
    {
        private static readonly string[] requiredScope = new string[] {"access_as_user"};
        private readonly ILogger<GraphAPIService> _logger;
        private readonly GraphServiceClient _graphServiceClient;
        public GraphAPIService(ILogger<GraphAPIService> logger, GraphServiceClient graphServiceClient)
        {
            _logger = logger;
            _graphServiceClient = graphServiceClient;
        }

        [Authorize]
        public override async Task<CalendarReply> GetCalendar(CalendarRequest request, ServerCallContext context)
        {
            var httpContext = context.GetHttpContext();
            httpContext.VerifyUserHasAnyAcceptedScope(requiredScope);

            if (!request.Name.Equals("Christos", StringComparison.OrdinalIgnoreCase))
            {
                // search the Graph for the user's calendar data
                // to-do
                return new CalendarReply();
            }

            var data = await _graphServiceClient.Me.Events
                .Request()
                .Select("subject,bodyPreview,start,end")
                .GetAsync();

            return new CalendarReply
            {
                Subject = data.First().Subject,
                BodyPreview = data.First().BodyPreview,
                Start = data.First().Start.DateTime,
                End = data.First().End.DateTime
            };
        }
    }
}
```
Now, let's update our `Startup.cs` to handle the token validation and include the Graph client. Since the Microsoft.Identity.Web integrates with Graph via the Microsoft.Identity.Web.Graph package, it's now a lot more straightforward to make the Graph client available in our app. Update the `ConfigureServices()` method with the following code:

```
services.AddGrpc();
services.AddMicrosoftIdentityWebApiAuthentication(Configuration)
        .EnableTokenAcquisitionToCallDownstreamApi()
     .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
        .AddInMemoryTokenCaches();
            
services.AddAuthorization();
```

## Update the gRCP Service App Registration in AAD
There are 2 more steps to complete this task. Firstly, we must update the App Registration in AAD to include the Graph API permissions. Secondly we need to update the `appsettings.json` file in our app so that we can call the downstream API (i.e Graph).

In the Azure AD portal, go to the App Registrations tab and find the gRPC Service app registration. Navigate to the **Certificates & Secrets** and create a new secret:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cqc4xjj59egmabcu34rk.png)

Next, we need to add the appropriate Graph API permission, i.e `Calendars.Read`. Navigate to the **API Permissions** tab and press the **Add a permission** button:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/3prliv2lq0ghyru4vmki.png)

Next, select **Delegated permissions**, search for `Calendars.Read` and press the **Add permissions** button to persist the changes

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/xqybo20cdvofmctwgm8e.png)

The final step is to **Grant admin consent for <user>**. Since this will be a Service-to-Service call, the user can't consent to these new permissions. Therefore, we need to pre-consent the new API permissions to allow the calls to succeed.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mj7n3oyld0158klblihw.png)

We don't need to update the gRPC Client app registration as nothing's changed for our client app in terms or permissions.

## Updates to the gRPC Service appsettings.json
With our app registration updated, we now need to update the appsettings.json file in the gRPC service to make use of the new settings. Open the file and update or add the following sections:
```
 "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "cmatskas.onmicrosoft.com",
    "TenantId": "b55f0c51-0000-0000-0000-33569b247796",
    "ClientId": "e7ba8202-0000-0000-0000-90ee506bc5cf",
    "ClientSecret": "PxgK83FL-0000_aaa_0000000000000000"
  },
  "DownstreamAPI": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": "user.read calendars.read"
  }
```
These are all the changes we need to do to our **gRPCWitAuth**, i.e the gRPC service to integrate with MS Graph.

## Update the gRPC Client app
As with the previous blog post, we need a *client* app to consume the service. Our GitHub repo included a `grpcClient` console app that acts as our gRPC client. 
The only step required here is to update the `Main` method to call the gRPC service using the new `GraphService`:

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

using static System.Console;

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 graphClient = new GraphService.GraphServiceClient(channel);
            
            var headers = new Metadata();
            headers.Add("Authorization", $"Bearer {token}");
            
            var calendarRequest = new CalendarRequest()
            {
                Name = "Christos"
            };

            var calendarResponse = await graphClient.GetCalendarAsync(calendarRequest, headers);
            WriteLine("*************** New event found ***************");
            WriteLine($"Calendar event subject: {calendarResponse.Subject}");
            WriteLine($"Calendar event body preview: {calendarResponse.BodyPreview}");
            WriteLine($"Calendar event start date/time: {calendarResponse.Start}");
            WriteLine($"Calendar event end date/time: {calendarResponse.End }");
        }

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

## Run it!!
Run the **gRPCWithAuth** service first. Then run the **gRPCClient**. Our console up will prompt for authentication. Follow the steps:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/bxhyhc94hu3lir6bbov6.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jyxa3n47t4eo99qscy7i.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/azd6abr3kg2n7lasc3as.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qdabcildvhf6jbok7jvj.png)

It worked! Now let's look at the service output and see what happened. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mmgr0cu9x4uy98i76ewi.png)

As you can see, the service received the incoming call, validates the access token, then authenticates for the downstream API (aka our MS Graph) and then executes the `/GetCalendar` request. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gge7f14ekohf8d5owgjq.gif)

## Give me teh codez
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/sl8012v6h4yt8ops4ce1.jpg)

If you want to grab the code, jump straight to the [GitHub repo](https://github.com/cmatskas/gRPCAuthWithAzureAD/tree/MSGraph). I have a new branch so that you can compare and contrast the old and the new branches.

One thing that you'll notice is different is that I've lately started adding Code Tours to the source code. This is an [excellent code extension](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) for VS Code that allows you to take a tour through the code and get explanations/annotation for code that is important to a task at hand. I would urge you to use it to understand how this project was put together and also to start using it yourselves to help other understand what you build

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/74312kg7h8jjbus8qij2.gif)

##Summary
It was great fun integrating Microsoft.Identity.Web and expanding the service to include Microsoft Graph. As you may have alluded from this blog post, the new library makes it extremely straightforward to implement authentication and add MS Graph. I would encourage you to go ahead and start using the new library to your ASP.NET Core projects today. 

And don't forget! You can catch me and JP on stream doing things like this. See you Twitch - https://aka.ms/425Show