The Problem

Perhaps, you have a web application that should allow access to user functions for customers, authenticated by one method, and to administrative functions for staff, authenticated by another method. For instance:

  • / — to everyone
  • /orders — to customers, authenticated with federated SSO
  • /admin — to staff, authenticated by windows (Active Directory)

At my work, Single Sign-On for customers is based on WS-Federation protocol (passive profile) and is separated from stuff windows authentication.

For such kind of authentication mixing in ASP.NET apps, the internet would give you ideas of using fake HttpResponse status code to skip federated authentication module processing for a non-authorized situation. It's often suggested the following algorithm:

  1. In the controller of /admin — if a client is not windows authenticated return response with status code 418, else do the normal job.
  2. In global.asax.cs in Application_EndRequest() method — if the status code is 418, set it again to 401.2, which means that server is favouring an alternative authentication method.

Simple, but:

  1. Windows authentication performed as a result of such 401.2 substitutions would be NTLM, but never Kerberos.
  2. It doesn't consider the case of support user walking from admin URL to customer URL and back, having customer's fed-auth cookies and Kerberos / NTLM authentication at the same time. How would correct principal (corresponding to URL) be reconstructed?
  3. It relies on fake code instead of some kind of authorization exception or filter attribute. This spoils structure of the code.
  4. It relies on the order of EndRequest pipeline event handling. We have no documented guarantees about that order.

I believe a double response status code change is the dirty hack.

How windows authentication works

Global picture

Firstly, Windows and IIS very optimized together to use hardware. Keeping of TCP connection can be off-loaded to the server-class network card (they have own TCP/IP stack support), while HTTP protocol is processed in kernel mode driver. This dramatically reduces the number of interruptions and context switches, giving more CPU power to perform your code.

Also, this architecture brings following features for security:

  1. Processing goes from kernel to w3wp.exe (running under application pool credentials) directly.
  2. Security Support Providers are implemented in the kernel, interfacing (trough SSPI) to kernel and w3wp.exe.

I made the following picture to illustrate this:

I put the green block "Managed code" inside of the white "w3wp.exe native code" block to illustrate that this is the same process (from OS perspective), where managed code is loaded by Common Language Runtime (which is native DLL itself) inside of w3wp.exe space. Therefore both native and managed IIS modules are available everywhere in IIS integrated pipeline (which is managed pipeline in fact).

NTLM, Kerberos and Negotiate protocols are implemented by Security Support Providers. Negotiate is not authentication protocol itself, it is used to negotiate one — Kerberos or NTLM. The only way to enable Kerberos in IIS is by enabling Negotiate for Windows Authentication.

By Kerberos there should be the key, belonging to a service account, to decrypt service access ticket (issued by KDC for a client). The service account is:

  1. If kernel mode authentication is enabled (by default) — SYSTEM (It is {MACHINE}$ from the perspective of Active Directory).
  2. If kernel mode authentication is disabled — application pool account. In this case, you need to generate SPN (Security Principal Name) for the domain user, under which you run application pool.

You can read more in ASP.NET Application Life Cycle Overview for IIS 7.0 (it is old but informative) or in the source code of HttpApplication, HttpRuntime and other related classes in Reference Source of .NET Framework. For Kerberos and SPN consult this article.

Module level picture

For every request, w3wp.exe process forks thread and associate pooled instance of HttpApplication class to it. Such OS thread can have or not windows user authentication token attached to it. Therefore, windows authentication is processed by two modules:

  1. Native WindowsAuthenticationModule (Inetsrv\Authsspi.dll) which works with SSPI to authenticate, hold a session and attach user authentication token to the thread.
  2. Managed WindowsAuthenticationModule which recreates principal in .NET based on the token in the thread.

How WS-Federation authentication works

Global picture

Despite the matter of protocols and using or not of Security Support Providers, from the perspective of an ASP.NET application, authentication is the presence of trusted information about the principal in HttpContext.Current.User and Thread.CurrentPrincipal.

Module level picture

Federated authentication (part of Windows Identity Framework living in System.IdentityModel.Services) is processed by two managed modules:

  1. WSFederationAuthenticationModule that takes care of token in request after redirection back from authentication service (STS in terms of WS-Federation protocol) and redirects to STS when needed. Recreates principal from token.
  2. SessionAuthenticationModule that creates a fed-auth cookie that holds authentication info during a session. Recreates principal from that cookie.

Federated authentication, like forms authentication, not related to SSPI at all.

Common to both federated and forms authentication mechanisms is that it:

  1. Handles Authenticate pipeline event and recreates .NET principal. Authenticate event is not a moment of authentication from the perspective of a user, it is the moment to recreate principal in .NET before processing of the request.
  2. Handles EndRequest pipeline event and starts authentication if the response to be sent has 401 status code.

Federated authentication module assigns constructed principal to HttpContext.User, then directly to Thread.CurrentPrincipal, while forms rely on some magic in assigning to Thread.CurrentPrincipal, that I don't understand (that doesn't happen in 0.001% of cases, see Scott Hanselman).

Nuances

Handlers of an Authenticate event in all Microsoft modules take care of existing user passed in HttpContext. If User != null module skips work because it means that the user was authenticated (somehow) by another module.

You can't strictly control the order in which modules handle EndRequest event. If you use some + windows authentication, I guess (experiments show it's true) native windows authentication module will handle status code 401 last.

Managed windows authentication module class is sealed and don't have events that could help us hook it up (unlike forms module).

Classes of federated authentication modules are open for extending.

How to mix windows and federated auth?

Firstly, create some configuration section to put the collection of URLs (admin URLs) in it, for which your skip federated and go for windows authentication:

<federationAuthenticationExclusions>
    <items>
      <add url="/admin" />
      <add url="/debug" />
    </items>
</federationAuthenticationExclusions>

Of course, you need to support this by section / collection / item objects in code.

Secondly, extend WSFederationAuthenticationModule to ignore handling of status code 401 for admin URLs:

using System;
using System.Collections.Generic;
using System.IdentityModel.Services;
using System.Linq;
using System.Web;
using WebApplication1.Configuration.FederationAuthenticationExclusions;

namespace WebApplication1.Modules
{
    public class FederationAuthenticationModule : WSFederationAuthenticationModule
    {
        protected override void OnEndRequest(object sender, EventArgs args)
        {
            HttpApplication httpApplication = (HttpApplication)sender;

            // step over federative authentication if URL in federationAuthenticationExclusions section of Web.config
            foreach (Item item in Section.Default.Items)
            {
                if (httpApplication.Request.RawUrl.StartsWith(item.Url, StringComparison.InvariantCultureIgnoreCase))
                    return;
            }
            base.OnEndRequest(sender, args);
        }
    }
}

Finally, extend SessionAuthenticationModule to not only skip for admin URLs, but also to clean httpApplication.Context.User if a user, being windows authenticated, visits URL requiring federative authentication:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading;
using System.Web;
using WebApplication1.Configuration.FederationAuthenticationExclusions;

namespace WebApplication1.Modules
{
    public class SessionAuthenticationModule : System.IdentityModel.Services.SessionAuthenticationModule
    {
        protected override void OnAuthenticateRequest(object sender, EventArgs eventArgs)
        {
            HttpApplication httpApplication = (HttpApplication)sender;

            // step over federative authentication if URL in federationAuthenticationExclusions section of Web.config
            foreach (Item item in Section.Default.Items)
            {
                if (httpApplication.Request.RawUrl.StartsWith(item.Url, StringComparison.InvariantCultureIgnoreCase))
                    return;
            }

            IIdentity identity = Thread.CurrentPrincipal.Identity;

            // in case of federative authentication (URL not in exclusions)
            // if user is authenticated, but it is not federative authentication, reset authentication
            if (identity.IsAuthenticated || identity.AuthenticationType != "Federation")
            {
                httpApplication.Context.User = null;
            }

            // if user is not authenticated, try to authenticate as usual by FedAuth cookie
            base.OnAuthenticateRequest(sender, eventArgs);
        }
    }
}

If a user goes to admin URL then to customer page, session module will try to reconstruct principal from fed-auth cookie normally.

If a user goes to customer page then to admin URL, session module will skip reconstruction, and normal windows authentication will happen.

The working example you can take at https://github.com/dmlarionov/IISMixedAuthExample.