Protecting legacy APIs with an ASP.NET Core Yarp reverse proxy and Azure AD OAuth

This article shows how a legacy API could be protected using an ASP.NET Core Yarp reverse proxy and Azure AD OAuth. The security is implemented using Azure AD and Microsoft.Identity.Web. Sometimes it is not possible to update an existing or old API within a reasonable price and the financially best way to use it in a public domain or using modern security is to use a reverse proxy and isolate the API through the proxy. Securing the API directly would always be the best solution if this is possible.

Code: https://github.com/damienbod/AspNetCoreYarp

Setup

The Yarp ASP.NET Core application uses the Microsoft.Identity.Web Nuget package to secure the reverse proxy and if a HTTP request has a valid access token, the HTTP request is forwarded to the legacy API. To test the reverse proxy, a simple ASP.NET Core Razor page application is used to authenticate against Azure AD, to get an access token using the ITokenAcquisition interface and use the access token to access the reverse proxy API.

ASP.NET Core Yarp reverse proxy

To implement the reverse proxy and secure it using the Azure AD IdentityProvider, we use two Nuget packages, Microsoft.ReverseProxy and Microsoft.Identity.Web.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.ReverseProxy" 
        Version="1.0.0-preview.7.20562.2" />
    <PackageReference Include="Microsoft.Identity.Web" Version="1.4.1" />
  </ItemGroup>

</Project>

In the Startup class of the ASP.NET Core Web application, the AddReverseProxy extension method is used to add the Yarp reverse proxy. The AddMicrosoftIdentityWebApiAuthentication adds the security bits for the JWT Bearer token auth using Azure AD and Azure App registrations.

public void ConfigureServices(IServiceCollection services)
{
	services.AddMicrosoftIdentityWebApiAuthentication(Configuration);

	services.AddReverseProxy()
		.LoadFromConfig(Configuration.GetSection("ReverseProxy"));
}

The Yarp reverse proxy is added to the Configure method like any other middleware or endpoints. The authentication and authorization middleware is added before the endpoints and after the routing.

public void Configure(IApplicationBuilder app)
{
	app.UseRouting();

	app.UseAuthentication();
	app.UseAuthorization();

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapReverseProxy();
	});
}

The configuration for the reverse proxy is added in the app.settings. This could also be added as code configuation. In this demo, one specific route is setup. The “/api/crazy” is used to map the HTTP requests to the cluster1. This route uses the default authorization as only one default JWT Bearer auth is setup. The cluster1 forwards requests to the legacy API, which in this demo is the localhost with the port 44316.

"ReverseProxy": {
	"Routes": [
	  {
		"RouteId": "route1",
		"ClusterId": "cluster1",
		"AuthorizationPolicy": "Default",
		"Match": {
		  "Path": "/api/crazy"
		}
	  }
	],
	"Clusters": {
	  "cluster1": {
		"Destinations": {
		  "cluster1/destination1": {
			"Address": "https://localhost:44316/"
		  }
		}
	  }
	}
},

The Microsoft.Identity.Web package uses the AzureAd configuration like in the documentation.

 "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "your domain",
    "TenantId": "your tenant id",
    "ClientId": "your app registration client id"
  }

The legacy APIs should match the reverse proxy configuration. This demo using the api/crazy route.

Testing ASP.NET Core Client

To test the reverse proxy with authentication, a ASP.NET Core Razor page UI was implemented. This project authenticates against Azure AD using an Azure App registration. The ITokenAcquisition is then used to acquire a token for the proxy. A HttpClient sends a GET request to the API. All goes good, the data is returned. The UI application only knows the proxy URL.

private readonly IHttpClientFactory _clientFactory;
private readonly ITokenAcquisition _tokenAcquisition;
private readonly IConfiguration _configuration;

public LegacyViaProxyService(IHttpClientFactory clientFactory, 
	ITokenAcquisition tokenAcquisition, 
	IConfiguration configuration)
{
	_clientFactory = clientFactory;
	_tokenAcquisition = tokenAcquisition;
	_configuration = configuration;
}

public async Task<JArray> GetApiDataAsync()
{
	try
	{
		var client = _clientFactory.CreateClient();

		var scope = _configuration["AspNetCoreProxy:ScopeForAccessToken"];
		var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { scope });

		client.BaseAddress = new Uri(_configuration["AspNetCoreProxy:ApiBaseAddress"]);
		client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
		client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

		var response = await client.GetAsync("api/crazy");
		if (response.IsSuccessStatusCode)
		{
			var responseContent = await response.Content.ReadAsStringAsync();
			var data = JArray.Parse(responseContent);

			return data;
		}

		throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
	}
	catch (Exception e)
	{
		throw new ApplicationException($"Exception {e}");
	}
}

The ASP.NET Core Razor page UI project initializes the authentication using the AddMicrosoftIdentityWebAppAuthentication method and the EnableTokenAcquisitionToCallDownstreamApi method .

public void ConfigureServices(IServiceCollection services)
{
	services.AddTransient<LegacyViaProxyService>();
	services.AddHttpClient();

	services.AddOptions();

	string[] initialScopes = Configuration.GetValue<string>(
          "AspNetCoreProxy:ScopeForAccessToken")?.Split(' ');

	services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
		.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
		.AddInMemoryTokenCaches();

The Azure AD settings are added to the app.settings. The ClientSecret is required so that the application can be validated. The ClientSecret is added to the user secrets of the project in Visual Studio.

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "your domain",
    "TenantId": "your tenant id",
    "ClientId": "your app registration client id"
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath ": "/signout-callback-oidc",
    //"ClientSecret": "your secret from your user secrets.."
},
"AspNetCoreProxy": {
    "ScopeForAccessToken": "api://your app registration client id for api/access_as_user",
    "ApiBaseAddress": "https://localhost:44345"
},

When all three applications are started, if you open the proxy with the correct address, a 401 is returned. This is what we want.

If the UI application logs into Azure AD and sends a HTTP request to the Yarp reverse proxy, the data from the legacy API is returned. Now the legacy app can be isolated and only made only visible to the proxy service.

Yarp is in preview and looks really promising. It fulfils many other use cases, typical to a reverse proxy. Go check out the yarp github repo and try it out. Here’s a link to the getting started documentation.

Links:

https://microsoft.github.io/reverse-proxy/articles/getting_started.html

https://github.com/microsoft/reverse-proxy

Introducing YARP Preview 1

https://channel9.msdn.com/Shows/On-NET/YARP-The-NET-Reverse-proxy

https://github.com/AzureAD/microsoft-identity-web/wiki

2 comments

  1. […] Protecting legacy APIs with an ASP.NET Core Yarp reverse proxy and Azure AD OAuth (Damien Bowden) […]

  2. […] Protecting legacy APIs with an ASP.NET Core Yarp reverse proxy and Azure AD OAuth – Damien Bowden […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: