Secure and minimal APIs using .NET 6, C# 10 and Azure Active Directory
Tags: dotnet, cloud, security
Published: 2021-06-02 17:52:32

Microsoft Build 2021 was insane and packed-full with new features, product announcements and capabilities. We also got a glance on the things that are to come in the coming year. I particularly enjoyed the sessions around .NET and Azure PaaS/FaaS! You can catch up on all the content on demand [here](https://docs.microsoft.com/events). 

In one of these sessions, I saw Maria Nagagga and Stephen Halter present a very minimalistic API built with .NET 6 and C#10. There are some exciting announcements around the new version of .NET and many language improvements. I have to admit that the new .NET 6 (currently in Preview 4) looks a lot like a barebones Node.js API which is great since we, the developers, have to write a lot less code to achieve what we need. Less code == less bugs!

I've toyed around with minimal APIs using the [FeatherHTTP](https://dev.to/425show/testing-the-api-waters-with-featherhttp-and-microsoft-identity-web-584k) framework, but having this capability built into the .NET framework, without needing to have to bring an external dependency :)

Inspired by Maria's and Stephen's session, I went ahead and not only created my own version of a minimal API but I also added authentication using Azure AD. Let's take it for a spin

### Prerequisites
You need to install the following:
- The latest .NET 6 (Preview 4)  from [here](https://dotnet.microsoft.com/download/dotnet/6.0)
- The latest Visual Studio 2019 preview 

I'm using VS Code so no guardrails or wizards for me. Luckily the .NET CLI has a template to support minimal APIs :) CLI-first baby!!!

Finally, many of the bits in this code are still using nightly bits so you'll need to create a `nuget.config` file and add the following XML to ensure that you can pull the right NuGet packages. Living dangerously is fun....

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!--To inherit the global NuGet package sources remove the <clear/> line below -->
    <clear />
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
    <add key="dotnet6" value="https://dnceng.pkgs.visualstudio.com/public/_packaging/dotnet6/nuget/v3/index.json" />
    <add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
  </packageSources>
</configuration>
```

### Do you want to build an API? (or a snowman)

Open your favorite command prompt and type the following

```
dotnet new web -n minimalapi
```
You should end up with something that looks like this:

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

That's as minimal as it gets straight "out of the box"! Open the `*.csproj` file and update it to the following:

```
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <LangVersion>Preview</LangVersion>
    <UserSecretsId>2bd37d96-2487-4c58-a5f3-ddd2524920ea</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Identity.Web" Version="1.11.0" />
    <PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.0.0-2.21275.18">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <Compile Include=".usings" />
  </ItemGroup>

</Project>
```
This allows us to target preview language features, pulls the compiler NuGet package to run and debug the application as wells the `Microsoft.Identity.Web` NuGet package for authentication.

Next, we'll add a `.usings` file where we can dump (I mean declare) all our usings. Nice a tidy. Mine looks like this:

```
global using System;
global using System.Collections.Generic;
global using System.Globalization;
global using System.Linq;
global using System.Net;
global using System.Security.Claims;
global using System.Net.Http;
global using System.Net.Http.Json;
global using System.Threading.Tasks;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Cors;
global using Microsoft.AspNetCore.Http;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Identity.Web;
global using Microsoft.AspNetCore.Authorization;
global using MinimalWeather;
```
Unlike traditional ASP.NET APIs, the minimal version doesn't come with a `Startup.cs`. Instead, all our initialization and middleware, as well as our endpoints, go into the `Program.cs` file. Open the file and add the following:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => options.AddPolicy("allowAny", o => o.AllowAnyOrigin()));
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddAuthorization();

var app = builder.Build();

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

app.MapGet("/secure", [EnableCors("allowAny")] (HttpContext context) => 
{
    AuthHelper.UserHasAnyAcceptedScopes(context, new string[] {"access_as_user"});
    return "hello from secure";
}).RequireAuthorization();


app.MapGet("/insecure", [EnableCors("allowAny")] () =>
{
    return "hello from insecure";
});

app.Run();
```
Notice the total absence of `usings`, `namespaces` etc etc. This is **beautiful**!

The code creates a WebApplication, we then add CORS and authentication with M.I.W. Literally in 3 lines of code. We then need to enable the authentication and authorization in the middleware and we finally define two endpoints:

- `/secure` (requires a valid access token)
- `/insecure` (can be accessed without prior authentication)

Unfortunately, since it's early days, the integration with Microsoft.Identity.Web is not fully backed. I was unable to use the `RequiredScope` attribute to force a check on the incoming JWT scopes. As such, I copied the code from the Microsoft.Identity.Web repo [here](https://github.com/AzureAD/microsoft-identity-web/blob/bba91c4298411e780c9f082e8e3843eacd8e543c/src/Microsoft.Identity.Web/Resource/ScopesRequiredHttpContextExtensions.cs) and created a helper method to check the HTTP request for the right scopes. The code is shown here:

```
namespace MinimalWeather
{
    public static class AuthHelper
    {
        public static void UserHasAnyAcceptedScopes(HttpContext context, string[] acceptedScopes)
        {
            if (acceptedScopes == null)
            {
                throw new ArgumentNullException(nameof(acceptedScopes));
            }

            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            IEnumerable<Claim> userClaims;
            ClaimsPrincipal user;

            // Need to lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads
            lock (context)
            {
                user = context.User;
                userClaims = user.Claims;
            }

            if (user == null || userClaims == null || !userClaims.Any())
            {
                lock (context)
                {
                    context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                }

                throw new UnauthorizedAccessException("IDW10204: The user is unauthenticated. The HttpContext does not contain any claims.");
            }
            else
            {
                // Attempt with Scp claim
                Claim? scopeClaim = user.FindFirst(ClaimConstants.Scp);

                // Fallback to Scope claim name
                if (scopeClaim == null)
                {
                    scopeClaim = user.FindFirst(ClaimConstants.Scope);
                }

                if (scopeClaim == null || !scopeClaim.Value.Split(' ').Intersect(acceptedScopes).Any())
                {
                    string message = string.Format(CultureInfo.InvariantCulture, "IDW10203: The 'scope' or 'scp' claim does not contain scopes '{0}' or was not found. ", string.Join(",", acceptedScopes));

                    lock (context)
                    {
                        context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                        context.Response.WriteAsync(message);
                        context.Response.CompleteAsync();
                    }

                    throw new UnauthorizedAccessException(message);
                }
            }
        }
    }
}
```

### Create the App Registrations in the Azure Active Directory 

Rather than showing the steps one at a time, we will be using .NET Interactive to configure Azure AD with 2 App Registrations. The .NET Notebook is attached to the repo so all you have to do is follow the instructions and run it to wire up AAD authentication.
For this sample, I went with a web api and a web app configuration. The web app is used to call the API securely

### Show me the code!
You can find a working solution on the 425Show GitHub. [Repo]( https://github.com/425show/MinimalWeather)

### Summary
I really look forward to building more fun projects with .NET 6 and C# 10. The framework is faster, cleaner and more powerful than before. And don't forget that with .NET you can build apps for all platforms and all types of apps! 

Let me know if you have any issue and happy hacking :)