OpenId Connect with Azure Active Directory and ASP.NET Core Multiple Service Instances
By: Lili Xu
Tags: Aspnetcore, Azure, Azure Active Directory, Openid Connect, C Sharp Programming

With the new Microsoft.Identity.Web library, it’s straightforward to configure the ASP.NET core web app using Microsoft identity stack.

services.AddMicrosoftIdentityWebAppAuthentication(Configuration);

While this service configuration in Startup.cs, it brings in the functionality of Microsoft identity platform integrated with AAD apps and by default it set up the openid connect with id token grant from Azure Active Directory.

According to the ASP.NET Core security implementation. OpenId Scheme has two main handlers, challenge handler and authentication handler.

OpenId Connect with AAD https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#protocol-diagram-sign-in

If user has no cookie or cookie expires, the request would first hit the challenge handler. The challenge handler would

Generate a nonce in service memory. Generate a new cookie from the generated nonce and drop the cookie .AspNetCore.OpenIdConnect.Nonce.{Value} on user’s browser. (cookie path=/signin-oidc for further validation on authentication completion call back) Redirect user request to login.microsoftonline.com with the origin request url as reply url and the nonce in request query.

request redirect to AAD with nonce cookie being dropped (above: request, below: response)

Once user complete the login operation on login.microsoftonline.com site, the site would

POST login state to login.microsoftonline.com/kmsi if user select “Keep Me sign in”. login.microsoftonline.com drops SignInStateCookie on user browser, so that user won’t get asked for login interaction next time when gets redirected. (The cookie lifetime could be configured as a tenant or single app registration/service principal policy) Redirect request to {service}/signin-oidc with POST request of application/x-www-form-urlencoded. This form includes the user jwt Id token in base64 format issued by AAD including the nonce.

AAD issues id token with nonce in a form and redirect /signin-oidc to complete the authentication

This POST request gets handled by OpenId Authentication Handler, the authentication handler would

validate the id token signature. Because the token was asymmetrically encrypted, service would fetch the public key from /openid-configuration and verify the token was signed by AAD’s private key. Once the signature validation passes, service would trust all the content within the token. validate the nonce cookie is matching the nonce in id token. this is to mitigate the risk of replay attack. verify the audience, tenant, issuer, etc.. based on the configuration.

After all of these validation passes, the success authentication ticket gets created with user’s claims information extracted from id token. The ASP.NET Core web service would need to issue an authentication cookie to user’s browser to maintain the user active login session.

The authentication cookie needs to be symmetric encrypted, the local authentication cookie was signed by ASP.NET Core Data Protection stack. While data protection stack provides the key management to manage the symmetric key for cookie data encryption.

The default key management implementation in ASP.NET Core is XmlKeyManager and it automatically creates new key in local machine’s directory and protected with Windows DPAPI when there’s no existing valid key.

If the service has multiple instances and hosted in web farm, each service instance would have a key generated in its own instance, protected with Windows DPAPI using it’s hosted machine’s account identity and signing cookie using the protected symmetric key.

As a result, the authentication cookie would only get recognized by the single instance which signs the cookie using its own key. when it comes to multiple instances, to prevent the cookie not recognized by the another instance. A simple solution is setup the session affinity. So that the gateway service, or the load balancer could forward the request to the same instance according to the affinity cookie, or the dictorty mapping for client ip address to a particular instance.

Because single service instance could go down any time, session affinity might not be reliable enough. The other option is to share the cookie signing key across multiple instances. As recommended by the official doc, the azure storage could be an option.

We would need to secure the access to persist keys in the blob account.

We could need the service’s managed identity, or a service principal to have exclusive Contributor role on the storage account. Protect the keys with certificate’s private key and the certificate need to be installed in certificate store across the instances. Because the keys need to be unprotected by any instance, Windows DPAPI using the local account identity could not serve such purpose any more. When service starts up, it retrieves the token from AAD using client credentials flow with certificate(or managed identity, secret) in https://management.core.windows.net/.default scope. Service would get the connection string with the contributor role through the request through Azure Resource Manager(ARM) using the token.

With this configuration, we configured the OpenId connect and share the key for signing the authentication cookie in multiple service instances scenario.