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>
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:
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 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
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)
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.
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
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.
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. π
The latest Visual Studio 2021 preview ==> 2021?
Good catch! All fixed :) 2019