DEV Community

Christos Matskas for The 425 Show

Posted on

Secure and minimal APIs using .NET 6, C# 10 and Azure Active Directory

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.

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 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
  • 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>
Enter fullscreen mode Exit fullscreen mode

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

Open your favorite command prompt and type the following

dotnet new web -n minimalapi
Enter fullscreen mode Exit fullscreen mode

You should end up with something that looks like this:

Alt Text

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 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);
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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 :)

Top comments (6)

Collapse
 
riccardo10 profile image
Riccardo

Nice article, Christos. Thanks.
One quick question, I am new to Azure AD (so please bear with me 😊).
With the code provided, how does Azure AD ensure automatic refreshing of tokens?

Thanks again.

Collapse
 
christosmatskas profile image
Christos Matskas

Hey @riccardo , when you acquire an Access Token, you also get a Refresh Token that is responsible for getting a 'fresh" access token when your access token expires

Collapse
 
riccardo10 profile image
Riccardo

Thanks for the response Christos.
I take it then that under the hood, the middleware (in collaboration with the identity platform) will manage the acquisition/disposal of access and refresh tokens in a seamless fashion, thereby allowing the UI to focus on business logic, yes?

Really amazing work you guys @ Microsoft are doing for the .NET developer community.

Thread Thread
 
riccardo10 profile image
Riccardo • Edited

Hi again Christos,
After watching some of your videos I can answer my last question now 😎
Yes, under the covers, the infrastructure handles token refresh and caching.

Thanks. 👍

Collapse
 
dalvarez profile image
Daniel Alvarez

The latest Visual Studio 2021 preview ==> 2021?

Collapse
 
christosmatskas profile image
Christos Matskas

Good catch! All fixed :) 2019