ASP.NET Mixed Authentication (ADFS and WindowsIntegrated)

In this article I will show you how to solve a problem I’ve recently stumbled upon. To my disbelief – there was no ready solution on Stack Overflow! The problem I’m talking about is having 2 authentication modules serving your authentication needs at the same time. I will also tell you a little bit on how IIS and .NET security works to give you some context. We are talking about older Integrated pipeline of .NET 4.X but similar concepts exist in Owin/Katana of new .NET CORE). Also, if you’d like to check the working code for this article you can download it from my GitHub here.

A little background

Before we go any further, I’ll introduce some actors that will play important parts in our scenario:

  • Domain Controllers. They are part of a Microsoft product called Active Directory. We will refer to them as AD and DC. DCs are dedicated machines that serve one purpose. That is managing permissions and access to your internal network resources. Basically when you login to your PC inside a school or work, often you are sending your credentials to a DC. Then it tells your PC if its ok to let you in and how much you should be allowed to do.
  • Active Directory Federation Services – ADFS. You can think of it as an add-on to Active Directory. AD is the whole suit of products related to authentication. ADFS allows your DC to authenticate in more ways (like SMS codes / 2FA / OpenID Connect) and on a larger scale, even outside of your local network.
  • Domain Trust. It’s a special kind of link that you can establish between two DCs that allows users in one network to authenticate with credentials stored in the other networks DC.

The definition of our problem is as follows. There is an existing .NET4 application using Windows Authentication mode, authenticating against its local DC. Some modules of this application are now to be used by another company’s users, so we need to authenticate them.

Since they will be accessing  our externally available portal URL in their local networks, they will authenticate against their local DC. Which is not a trusted authority to our application. This means they won’t be able to access our system with their windows domain credentials. Or to be more precise: with Security Tokens generated for them by their local DC.

What choices do we have?

One solution to this problem would be establishing a domain trust between 2 networks, even a one-way trust would do in this scenario. In our case this wasn’t an acceptable solution due to some differences in security policies between the two companies. Another possible solution we have considered was setting up 2 instances of our portal.

Second instance would be basically a clone of first one, but we would modify web.config to use FederatedAuthentication module allowing us to authenticate using the external company’s ADFS server. They would have to set one up as they only had a regular DC setup in their network.

Second solution

Second solution was acceptable but didn’t seem elegant. Not to mention the costs of second server setup and maintenance. We’ve established that the external company was willing to setup ADFS for us to connect to, so the only problem left to solve was the unnecessary second server and portal instance. This single-authentication-mode limitation was introduced by ASP.NET and IIS so we have started looking closer at the process of authentication trying to find some extensibility mechanism. Looking at this Architectural overview of IIS and its integration with ASP.NET it seems we should be able to inject our own custom module at any point in the request processing pipeline – that’s pretty good news.

Digging even further in the IIS Modules Overview we found some information on how and when modules are processed. The whole IIS Pipeline for ASP.NET raises notifications on every step. This enum contains the NotificationTypes emmited by IIS in the request processing pipeline. Our notification stages are also visible on this request life cycle diagram here:

That’s great, we now understand there are extension mechanisms on every stage of the request processing pipeline! Documentation also states (and this is important) that there has to be at least one authentication module configured for any application. Surely this means there can be more, more good news! Looking at the architecture, documentation and decompiling some modules we have established that modules (at least authentication modules) are executed in a chain ordered by their position in the “web.config -> configuration -> system.webServer -> Modules” section. Modules are executed until one of them does not flag a successful authentication and break the chain.

Implementation time!

Based on our research so far, I think it’s safe to say we are ready to start coding. We’ll create a custom version of the WSAuthenticationModule. It’s a module able to authenticate against ADFS servers. We’ll place it as a first one in the list of modules in web.config and add a little routing logic into it. We won’t modify any authentication logic as this would be irresponsible. The only thing we will add is a simple IF statement in some key methods which we will be overriding. Our “IF statement” a.k.a. “authentication routing logic” will basically look for a custom header we came up with (we called it “ExternalAuthentication”). If the header is found, we will execute base logic of each method we override. This will allow a basic ADFS authentication. We can inject this header to any request coming from the external company in our load balancers or any other network component between us and them.

Now, if the header is not present in the request – we will simply “return” not executing any logic. This will automatically pass the request to the next configured authentication module which in our case will be WindowsAuthentication.

protected override void OnAuthenticateRequest(object sender, EventArgs args)
       {
           var preferredAuthenticationModule = AuthenticationRoutingService
               .GetPreferredAuthenticationModule(((HttpApplication)sender).Context);
           if (preferredAuthenticationModule == AuthenticationModuleTypes.WSFederation)
           {
               log.Debug("Executing native OnAuthenticateRequest");
               base.OnAuthenticateRequest(sender, args);
           }
           else
           {
               log.Debug("Bypassing native OnAuthenticateRequest");
           }
       }

The GetPreferredAuthenticationModule method is simply inspecting the request looking for our custom header. Below is a high level overview of both supported authentication flows:

Testing Ground

For our testing we have used a VMWare workstation player on which we have created a clean image of Windows Server 2019 Datacenter Edition. Here are steps we have taken with links to materials going more into details of how to achieve our testing setup:

  1. Download VMWare Workstation Player and download a Windows 2019 Datacenter ISO image from Microsoft evaluation center.
  2. Install Windows server (if you’ll get the licence error, just follow these steps) select the “Desktop Experience” in the installation type selector.
  3. Add the Domain Controller role to the server.
  4. Add the ADFS role to the server.
  5. Create and deploy simple ASP.NET MVC app to your local IIS.
  6. In the AD FS Management app on your server, add Relying Party Trust. This is a process of introducing your app to the ADFS. Without this step your application will not be able/trusted to connect to the ADFS at all.
  7. Add our security module and fill the configuration section as per example in the GitHub here.
  8. Modify global.asax to load our configuration as per example on my GitHub.
  9. Download Chrome extension allowing modifying headers on outgoing requests (this is the one I used)
  10. Test what happens when you access your page with the header present and absent on your requests. Make sure you hard refresh page if you add or remove header as credentials are heavily cached by both browser and the system.

Below you can check the results of my test.

This is where we add or remove the “ExternalClient” cookie (value is not important).

Here we authenticate against the Mortycorp – the external company.

And here we authenticate against our local domain controller.

That’s pretty much it, hopefully you will find it useful and even though my code is for the older .Net/IIS pipeline (not .NET core), this module can easily be reused as a middleware with the new OWIN/Katana pipeline in .net core. If you’d like to read about OWIN and Katana in scope of mixed authentication, please let me know in the comments below.

Tags: