About 3 years ago, I wrote some code for a customer. It was a sample solution. The front end is a straight up ASP.NET Core MVC app. The backend API is a simple .NET Core API. Both make use of Open ID Connect (OIDC) and OAuth2 to authenticate users and acquire tokens in order to retrieve and update a ToDo list (so original - I know!). However, the "juice" of the solution is in the authentication code. Therefore, in this blog post, we'll look at how we can take out the "old" authentication code and replace it with the latest and greatest in ASP.NET Core - Microsoft.Identity.Web. The blog comes in two parts:
- Part 1 - Updating the Web App authentication code
- Part 2 - Updating the Web API authentication code
Part 1 will focus on upgrading the authentication of front-end ASP.NET MVC web app as per this animation:
Updating the Web App
Thanks to the innovative work that has been put in Microsoft.Identity.Web, the migration will allow us to remove a lot of the existing code. Anything that had to do with OIDC can now go. Most of the work will focus on upgrading the project to .NET 5 and pulling the right dependencies. The following image shows where the bulk of the work will be focused:
Once we remove all the marked files, we can go ahead and start the migration. Open the *.csproj file and update from
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<UserSecretsId>aspnet-WebApp_OpenIDConnect_DotNet-81EA87AD-E64D-4755-A1CC-5EA47F49B5D8</UserSecretsId>
<WebProject_DirectoryAccessLevelKey>0</WebProject_DirectoryAccessLevelKey>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
</Project>
to this:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.0" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.0" NoWarn="NU1605" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.3.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.4" />
</ItemGroup>
</Project>
Next, we need to update the appsettings.json
to simplify things a bit. Replace the old config with the following:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "https://<TenantName>.onmicrosoft.com",
"TenantId": "<YourTenantID>",
"ClientId": "<YourClientID>",
"CallbackPath": "/signin-oidc",
"ClientSecret": "<YourClientSecret",
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
}
}
The Startup.cs
class in ASP.NET 5 has changed quite a bit so we need to update quite a few things, not just the code related to authentication. First, we need to update the ConfigureServices()
method as per below:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.Configure<MicrosoftIdentityOptions>(options => options.ResponseType = OpenIdConnectResponseType.Code);
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
services.AddControllersWithViews()
.AddSessionStateTempDataProvider()
.AddMicrosoftIdentityUI();
services.AddSession();
}
First, we need to configure Microsoft.Identity.Web to use Azure AD for authentication and set up a cache to store the Access Token for the API calls. The the .AddMicrosoftidentityUI
instructs the middleware to use the auth views embedded in the M.I.W dll.
Next, we need to update the Configure()
method as per the code below:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
We should also update the Program.cs
while we're at it. Replace the whole code with this:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace WebApp_OpenIDConnect_DotNet
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
With the middleware sorted, we can now move on to the Controllers and Views in our web app. First, we must update the Views->Shared->LoginPartial.cshtml view so that our login and logout pages can work as expected. The new code should look like this:
@using System.Security.Principal
@if (User.Identity.IsAuthenticated)
{
<ul class="nav navbar-nav navbar-right">
<li class="navbar-text">Hello @User.Identity.Name!</li>
<li><a asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a></li>
</ul>
}
else
{
<ul class="nav navbar-nav navbar-right">
<li><a asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="Signin">Sign in</a></li>
</ul>
}
Finally, we need to fix our ToDo Controller code since a lot of things have changed. The update code for our controller is provided below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
using TodoListWebApp.Models;
using System.Text;
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
namespace TodoListWebApp.Controllers
{
[Authorize]
public class TodoController : Controller
{
private static ITokenAcquisition tokenGetter { get; set; }
private static HttpClient httpClient;
public TodoController(ITokenAcquisition tokenAcquisition, IHttpClientFactory clientFactory)
{
httpClient = clientFactory.CreateClient();
tokenGetter = tokenAcquisition;
}
public async Task<IActionResult> Index()
{
var token = await tokenGetter.GetAccessTokenForUserAsync(new []{
"api://9a080a62-e244-4adc-a7d9-dc98835c8815/access_as_user"
});
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
var result = await httpClient.GetAsync("https://localhost:44351/todolist");
var content = await result.Content.ReadAsStringAsync();
var todoItems = JsonSerializer.Deserialize<List<TodoItem>>(content);
return View(todoItems);
}
[HttpPost]
public async Task<ActionResult> Index(string item)
{
if (ModelState.IsValid)
{
var token = await tokenGetter.GetAccessTokenForUserAsync(new []{
"api://9a080a62-e244-4adc-a7d9-dc98835c8815/access_as_user"
});
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var todoItem = new TodoItem { Title=item, Owner = User.GetObjectId()};
var content = new StringContent(JsonSerializer.Serialize(todoItem), Encoding.UTF8, "application/json");
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var result = await httpClient.PostAsync("https://localhost:44351/todolist",content);
if(result.IsSuccessStatusCode)
{
return RedirectToAction("Index");
}
else
{
return View("Error");
}
}
return View("Error");
}
}
}
Firstly, we introduce a constructor so that we can inject the ITokenAquisition
and IHttpClientFactory
objects to be used later in the code. The ITokenAcquisition class allows us to easily grab an access token from Azure AD/B2C in a single line of code whereas the IHttpClientFactory class is used to provide an HttpClient
and manage its lifecycle as per recommended practices.
Once the access token is acquired, we add it to the HttpClient
as an Authorization
header before making the request to the API.
The rest of the code either gets or posts ToDo items to the API.
Source Code
There is a fully working solution showing how the code works end to end on GitHub.
Conclusion
It's an irrefutable fact that Microsoft.Identity.Web has made developers' lives much easier when it comes to authenticating users and acquiring or managing tokens. This blog post is a proof that with the latest library, we have a lot less code to write in order to secure our application against Azure AD or B2C. In part 2, we will examine the changes needed to migrate our ASP.NET Core API to also use Microsoft.Identity.Web to validate incoming access tokens
Top comments (2)
Thanks for the article. I would strongly disagree with your assessment that Microsoft made things easier; what they did is turn a black box called Identity 2 (a somewhat extensible but lacking documentation to do so) implementation into a complete black magic! The Identity 2 was the reason that I felt so disadvantaged and dumbfounded since at least 2013, so much so that I had to literally drop everything for a complete month and study OAuth 2 framework to be able to start to comprehend that all that Microsoft did is help us "walk the last mile" to create and store an authentication ticket and store/register the users. But all this "extensible" data store and configuration had obfuscated the simplicity of the OAuth2 framework (along with the OpenIdConnect extension). Pair this with an inability to separate the UI from the database connection (something that arguably has no place in the UI tier unless it's a tiny throw-away app) and you get a classic case of disservice. So thank you for the overview, but no thank you to Microsoft.
Hi @timmi4sa , thanks for your comment. I'm sorry you had this experience with the Microsoft Identity platform. The whole point of the new MSAL libraries and, in pariticular, Microsoft.Identity.Web is to hide most of the complexities and enable developers to quickly get off the ground when it comes to authenticating and authorizing users. However, the individual libraries that are needed to interact with the OAuth and OIDC flows are still there if you have more advanced needs and need to customize the process. You don't need to if you don't want/have to but everything's still there.
Let us know if you have specific feedback that you would like to share with us and we'll make sure it get's passed to our engineering teams. thx