.NET Authentication Loop with Container Based Apps

Following on from my previous post on identity with .NET and containers, I wanted to expand on this to look at authentication loops from Entra ID.

The symptoms of this issue are pretty simple, when you hit your .NET container based application, secured with Entra ID, you are sent in a loop to the authentication page and eventually hit an error. Upon inspection of your container logs, you will see the following message.

Error from RemoteAuthentication: Unable to unprotect the message.State

What is happening?

Much like in Azure App Services, when the application scales out, without the ARR affinity setting configured, the request is going back to a different container from the one it started.

This causes an authorization failure and Microsoft.Identity.Web redirects back to Entra ID which re-authenticates the user and redirects them back to the app, this time hitting the other App Service backend instance and causing the same error above.

ARR affinity is a feature on Azure App Service that allows an end user to talk to the same Azure App Service worker instance until session finishes.

The same can happen on containers which sit behind load balancers, in this case, Azure Front Door. The cause, and the symptom is also the same.

Upon receiving the response from Entra ID, the .NET Core middleware takes care of validating the ‘state’ parameter to prevent cross-site forgery attack. The OpenID Connect OWIN middleware uses .NET Data Protection to encrypt the value stored in the ‘state’ parameter. However, data protection (DP) keys are not automatically synchronised across backend instances of the same app.

Let’s now look at how to resolve this option, with a couple of solutions.

Solution 1: Enable session affinity

On your origin group, open the configuration and check the box to Enable session affinity. You can see this in the screenshot below.

That’s all there is to it. For legacy applications, this would be a great way to resolve the issue, a simple way to resolve the issue as well.

That said, if you have the ability to update the code of the application, then it makes more sense to update the code, making use of the Azure platform to synchronise your data protection keys across the backend containers.

Option 2: Implement DP key handling

The second option, and if you have access to the source code, the preferred option is to enable synchronisation of your data protection keys across your backend instances.

There are a number of ways to do this, you can see through the documentation. For this example, we’ll make use of the Azure platform, storing the keys in Azure Storage and encrypting them with a Key Vault key.

// Add data protection keys
builder.Services.AddDataProtection()
.SetApplicationName("YourAppName")
.SetDefaultKeyLifetime(TimeSpan.FromDays(30))
.PersistKeysToAzureBlobStorage(new Uri(builder.Configuration["DataProtectionKeysUri"]), new DefaultAzureCredential())
.ProtectKeysWithAzureKeyVault(new Uri(builder.Configuration["DataProtectionVaultIdentifierUri"]), new DefaultAzureCredential());
view raw Program.cs hosted with ❤ by GitHub
The code above can simply be added to your Program.cs file, you will need to add the following packages.

Once these are added to your application, you will need to provide the URI to firstly your storage account path you want to store the file in. You can see in my code, I’m using a configuration value, which is actually stored in Azure App Configuration.

For Key Vault, you need to generate a new key and add the URI for the key into another configuration value. We are using the DefaultAzureCredential method, which is part of Azure.Identity, and allows us to use our local configuration for testing and the service managed identity when deployed.

As always, try to go with a least privileged model, which for the storage means adding the principals contributor access only on the container, and for the Key Vault, only allowing access to keys.

Leave a comment

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