Freigeben über


Local Debugging of .NET Core Web App with Easy Auth

Azure App Service is a great platform for managed hosting of web applications. One of the features of the platform is App Service Authentication using a variety of authentication providers such as Azure Active Directory, Google, and Twitter. It is possible to enable authentication without changing a single line of application code. This is also know as Easy Auth.

If you use Easy Auth with your application, you have access to user details and tokens through the token store (if enabled). After authentication you can hit the /.auth/me endpoint and obtain user name, id token, access token, etc. As I have discussed in a previous blog post, you can use the tokens to access backend web APIs such as Microsoft Graph or your own custom APIs.

If you chose to leverage the provided tokens and login details in your application, you may find that local debugging of your application can be a bit of a hassle. If you run the application locally on your development system, Easy Auth will not be available and you will not have the access tokens, etc. that you may need in your application. In order to debug those features of your application, you will need to deploy to an Azure Web App. An alternative approach is to do the login and authentication workflow in the application code, but then you are no longer leveraging Easy Auth.

I this blog, I will discuss an approach that I use. It is a bit of a hack but it does allow me to do a lot of debugging cycles locally before deploying and it speeds up my development. You may find that you can leverage my approach or just use it as inspiration to come up with your own approach.

I will use an example from my previous post on accessing custom backend APIs from an application using Easy Auth. In that example, I explained how to write and deploy an API and then use Easy Auth to obtain a token to access it from a Web App. In the application code, I used the x-ms-token-aad-access-token request header as authorization to access the backend API. That header is only available if Easy Auth is enabled. The application code was pretty simple, so it did not take a lot of debugging cycles to get it working, but in more complex scenarios may take a lot of debugging where it would be nice to have those header fields available locally.

The approach that I have found useful is to access those header fields through a proxy that will use the Easy Auth header fields when available and otherwise populate from a local file, specifically, I will create a file wwwroot/.auth/me in my source code repository. To populate this file, I leverage previously deployed version of the application where Easy Auth is enabled. If you log into a deployed application, you can get all tokens, etc. from the /.auth/me endpoint. I take the content returned from that endpoint and copy it into my local file. If you now run your application locally, you should have access to the same endpoint for tokens, etc. and if you are developing single page app in something like angular, this alone maybe be enough for you to do your debugging. But if you have server code that needs to leverage the request header fields as mentioned above, we have to take it a few steps further and implement a simple proxy. I have published a revised version of the application that uses a proxy. Here is how it works:

Let's consider the following function, which calls a backend API to delete a list entry:

[csharp]
public async Task<IActionResult> Delete(int id)
{
string accessToken = Request.Headers["x-ms-token-aad-access-token"];

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.DeleteAsync("https://listapi.azurewebsites.net/api/list/" + id.ToString());

return RedirectToAction("Index");
}
[/csharp]

What I do is that I access the header field through a proxy:

[csharp]
public async Task<IActionResult> Delete(int id)
{
string accessToken = _easyAuthProxy.Headers["x-ms-token-aad-access-token"];

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.DeleteAsync("https://listapi.azurewebsites.net/api/list/" + id.ToString());

return RedirectToAction("Index");
}
[/csharp]

This proxy can now be used to implement logic that will provide the header fields from the local file when that file is present. This can be a very simple class such as:

[csharp]
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace ListClientMVC.Services
{

public class AuthClaims {
public string typ { get; set; }
public string val { get; set; }
}

public class AuthMe {
public string access_token { get; set; }
public string id_token { get; set; }
public string expires_on { get; set; }
public string refresh_token { get; set; }
public string user_id { get; set; }
public string provider_name { get; set; }
List<AuthClaims> user_claims { get; set; }
}

public interface IEasyAuthProxy
{
Microsoft.AspNetCore.Http.IHeaderDictionary Headers {get; }
}

public class EasyAuthProxy: IEasyAuthProxy
{
private readonly IHttpContextAccessor _contextAccessor;
private readonly IHostingEnvironment _appEnvironment;
private IHeaderDictionary _privateHeaders = null;

public EasyAuthProxy(IHttpContextAccessor contextAccessor,
IHostingEnvironment appEnvironment)
{
_contextAccessor = contextAccessor;
_appEnvironment = appEnvironment;

string authMeFile = _appEnvironment.ContentRootPath + "/wwwroot/.auth/me";
if (File.Exists(authMeFile)) {
try {
_privateHeaders = new HeaderDictionary();
List<AuthMe> authme = JsonConvert.DeserializeObject<List<AuthMe>>(File.ReadAllText(authMeFile));

_privateHeaders["X-MS-TOKEN-" + authme[0].provider_name.ToUpper() + "-ID-TOKEN"] = authme[0].id_token;
_privateHeaders["X-MS-TOKEN-" + authme[0].provider_name.ToUpper() + "-ACCESS-TOKEN"] = authme[0].access_token;
_privateHeaders["X-MS-TOKEN-" + authme[0].provider_name.ToUpper() + "EXPIRES-ON"] = authme[0].expires_on;
_privateHeaders["X-MS-CLIENT-PRINCIPAL-ID"] = authme[0].user_id;
} catch {
_privateHeaders = null;
}
}
}

public IHeaderDictionary Headers {
get {
return _privateHeaders == null ? _contextAccessor.HttpContext.Request.Headers : _privateHeaders;
}
}
}
}
[/csharp]

To make this proxy available in a controller, I use Dependency Injection. In the Startup.cs file, the proxy is added as a service:

[csharp]
public void ConfigureServices(IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IEasyAuthProxy, EasyAuthProxy>();
services.AddMvc();
}
[/csharp]

And if you would then like to use it in a controller, you can do something like this:

[csharp]
public class ListController : Controller
{

private IEasyAuthProxy _easyAuthProxy;

public ListController(IEasyAuthProxy easyproxy) {
_easyAuthProxy = easyproxy;
}

//Rest of the class
}
[/csharp]

I keep the proxy class described above handy and I add it to many of my .NET Web Apps to make my debugging easy. In some cases I will do some application specific modifications to it, but it most cases I use it without any changes. When the app is deployed to an Azure Web App, it will leverage the header fields provided by Easy Auth, when I debug it locally I can still test many scenarios.

I quick note on token expiration. The information available from the /.auth/me endpoint contains tokens with expiration time. Consequently, the information you copy to the local file will quickly become stale. Depending on the timeout parameters, you will need to update your local information from time to time.

As I mentioned in the beginning, this approach is a bit of a hack, but it does allow me to develop faster, so I thought I would share it. If you have a similar or better approach, I would love to hear about it. In general, let me know if you have questions/comments/suggestions.