Migrating from old authentication code in ASP.NET with Microsoft.Identity.Web and Azure AD
Tags: azure, security, webdev
Published: 2021-01-05 01:20:18

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](https://github.com/AzureAD/microsoft-identity-web). The blog comes in two parts:

1. Part 1 - Updating the Web App authentication code
2. [Part 2](https://dev.to/425show/modernize-your-legacy-api-authentication-with-microsoft-identity-web-and-azure-ad-9ia) - 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:

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

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

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

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](https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.web.itokenacquisition?view=azure-dotnet-preview) class allows us to easily grab an access token from Azure AD/B2C in a single line of code whereas the [IHttpClientFactory](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.ihttpclientfactory?view=dotnet-plat-ext-5.0) class is used to provide an `HttpClient` and manage its lifecycle as per [recommended practices](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0). 

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. 
- [Before the migration](https://github.com/cmatskas/aspnetcoreaadauth)
- [After the migration](https://github.com/cmatskas/aspnetcoreaadauth/tree/net5.0)

## 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