Mixed Mode ADFS 2.0 and Forms Authentication in a Single ASP.NET Web Application

One project I worked on was for a custom Software as a Service (SaaS) application that needed to support federated log-in for some tenants, but standard forms authentication for others. After extensive research (aka ya-goo-bing) , it always looks like ASP.NET applications can only support one authentication mechanism at a time. This seemed stupid to me (and in particular because I was working on a SaaS app that requires something else) so I looked for ways to get around this. Its probably important to note that this isn’t the only reason you might want to support ADFS along with another security mechanism – other reasons might include:

  • Supporting single sign-on users from within the corporate network across both intranet and cloud applications without having to re-enter credentials
  • Perhaps your IT department doesn’t allow you or want you to add temporary contract workers to AD but yet they need access to an application
  • And, of course multi-tenant SaaS

First the use case – in this case, a multi-tenant SaaS application wants to allows some customers (tenants) to authenticate using ‘normal’ forms authentication. Other customer wish to federate their active directory using ADFS (or another WS-Federation compliant service that works with ADFS). Anyway, the problem is two fold – how to pre-identify the tenant so we know whether to send them down the ADFS route or the Forms Authentication route and secondly how to make the 2 authentication mechanisms play nice with one another.

My first attempt was with ADFS (v1 I think – the version that’s included with Windows Server 2008 R2) and that failed miserably – the http module included with that version (System.Web.Security.SingleSignOn.WebSsoAuthenticationModule) is sealed and thus wouldn’t allow me to override and inject the logic I needed to make a hybrid authentication solution work.

So, ADFS v2.0 was released and is based on Windows Identity Foundation. This looked more promising and I was able to build a solution on this. Here’s how I did it.

First I had to dig into how Forms Authentication does what it does – I think a sequence diagram for a request is in order here to show where it issues redirects – this is basically what forms authentication (alone) does to login…

Forms Authentication Sequence

Figure 1: Forms Authentication Sequence

In the diagram above, the following takes place:

  • A request comes in and the URL auth module indicates that access isn’t allowed (via the <deny users=”?”/> in web.config under system.web/authorization (normal asp.net authentication).
  • in response, it sends a http 401 error -
  • the browser will redirect to the log-in page.
  • everything from this point is pretty common knowledge and there are plenty of articles out there on forms auth ;)

Now, let’s have a look at how ADFS does its thing when enabled by itself

Figure 2: ADFS Authentication Sequence

OK, so this works basically the same as forms authentication except the redirect is to the ADFS server log-on service url (which will do the realm discovery and log-in stuff that ADFS does).  This is basically step 1 in an ADFS Passive Requestor Profile (a WS-Federation piece that uses browser redirects to sign in with ADFS).

OK, so finally the proposed solution; the idea here is to override some of the functionality in the ADFS http module in EndRequest so we can determine what type of authentication to use.  The sequence diagram will look like this:

Combination ADFS and Forms Authentication

Figure 3: Combination ADFS and Forms Authentication

OK, so to realize this, I had to implement a http module that derives from the WIF WSFederationAuthenticationModule – that code is below.



public class MyFederationModule : WSFederationAuthenticationModule
{
    /// <summary>
    /// Overrides the handling of the WIF federation module's end request 
    /// - basically if we receive
    /// a http 302 and the target redirect is Accont/Login.aspx then
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    protected override void OnEndRequest(object sender, EventArgs args)
    {
        HttpApplication app = (HttpApplication)sender;
        HttpRequest req = HttpContext.Current.Request;
        HttpResponse resp = app.Response;
        if (app.Response.StatusCode == 302 
		&& !string.IsNullOrEmpty(app.Response.RedirectLocation) 
		&& resp.RedirectLocation.ToLower().Contains("account/login.aspx"))
        {

            string customer = req.Cookies["customer"].Value;
            // first we need the customer - then we'll get redirected back here
            if (string.IsNullOrEmpty(customer))
            {
                HttpContext.Current.Response.Redirect(
		    "~/PromptForCustomer.aspx?redirect="+
		    app.Response.RedirectLocation, false);
                return;
            }
            bool isAdfs = GetCustomerSecuritySetting(customer);
            if (isAdfs)
            {
                // before we call base, set status code to 401 
		//which is what it expects when it needs to kick in
                app.Context.Response.StatusCode = 401; 
                base.OnEndRequest(sender, args);
            }
            else
                return; // forms authentication
        }
    }

    private bool GetCustomerSecuritySetting(string customer)
    {
        // TODO: lookup the customer by name and see if they are federated 
        if (customer.Equals("Federated"))
            return true;
        else
            return false;
    }
       
}

What happens here is pretty simple, on EndRequest we check to see if the http response code is 302 (redirect) and the redirect location is the forms auth login page – that’s our que to kick in and check what customer it is – the example just uses a cookie called customer that is set from a forms page called PromptForCustomer.aspx – the job of that page is to prompt for the customer (or however you will determine whether adfs is used or not) and set the cookie and then redirect back to the original location – that’ll cause the logic to run again. For that page to work it will have to allow anonymous access with a tag in web.config so the URL authorization module allows the redirect to go there.

Beyond that, you have to have forms authentication turned on and setup the web site with WIF STS Reference (a menu item on the project when you have WIF installed – it references the ADFS federation metadata).

That’s about it. Enjoy

This entry was posted in .NET Development, Security and tagged , , . Bookmark the permalink.

10 Responses to Mixed Mode ADFS 2.0 and Forms Authentication in a Single ASP.NET Web Application

  1. Thanks Dave for the article.
    So this set up may already cover what I want to do but to be sure I’ll ask anyway.

    ADFS allows single sign on from a customer’s corporate network to the provider’s SAAS, without any integration of the customer’s AD into the provider’s AD. What if a user from that customer wanted to log in to the SAAS from home, from a hotel, from some PC that was not on the corporate network?

    thanks.

    Roger

    • Sorry – just got back to my blog and saw this – I believe that would require the IdP (Identity Provider) be exposed publicly to authenticate the request – generally this is just a forms based login to the corporate AD – the the ADFS trust can be setup so the same redirect URL (the URL the SaaS redirects to for trusted authentication) were the same for both intranet and internet that’s ideal – I suppose another way is to require users to have a vpn connection to the corporate network.

  2. Doogal says:

    Thanks, that was really useful. One other thing I’m trying to achieve is the ability to support multiple ADFS authentication providers, so a user hitting /CustomerA logs in with their ADFS and a user hitting /CustomerB logs in with their ADFS. Did you ever get as far as implementing that? Any clues would be most weclome

  3. Richard says:

    That is an awesome article. I will try and implement this. I need to set a value in a lookup table somewhere in my app to determine if the application should try and use forms auth or ADFS. So, in the model I am planning, I need to set the application to pursue one mode of authentication or the other up front.

  4. saswat says:

    The article you have is way forward for me. We have similar situation. I have deployed claim aware application and added adfs resource partner to that site. That is working.
    For Achieving this solution you have given:
    Do I have to install WIF,
    using STS I have to redirect to adfs rather that directly adding adfs claim aware agent?
    Thanks in advance.

  5. Deependra Sampang says:

    I created MyFederationModule class and also added the same in web.config file

    But while executing the application getting following exception

    Object reference not set to an instance of an object.
    Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

    Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

    Source Error:

    An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

    Stack Trace:

    [NullReferenceException: Object reference not set to an instance of an object.]
    Microsoft.IdentityModel.Web.SessionAuthenticationModule.InitializePropertiesFromConfiguration(String serviceName) +29
    System.Web.HttpApplication.RegisterEventSubscriptionsWithIIS(IntPtr appContext, HttpContext context, MethodInfo[] handlers) +479
    System.Web.HttpApplication.InitSpecial(HttpApplicationState state, MethodInfo[] handlers, IntPtr appContext, HttpContext context) +335
    System.Web.HttpApplicationFactory.GetSpecialApplicationInstance(IntPtr appContext, HttpContext context) +349
    System.Web.Hosting.PipelineRuntime.InitializeApplication(IntPtr appContext) +381

    [HttpException (0x80004005): Object reference not set to an instance of an object.]
    System.Web.HttpRuntime.FirstRequestInit(HttpContext context) +11318198
    System.Web.HttpRuntime.EnsureFirstRequestInit(HttpContext context) +88
    System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context) +4348404

    Could you please let us know why I am getting this issue?

  6. Dev says:

    Hi Dave, I tried to achieve above scenario but no luck as am not sure where to place the code you provided. I just created a class file and placed the code sample. It will be helpful if you provide some sample application code or steps to do it.
    I have a similar situation where I have to mix ADFS and Windows Authentication.

    • Well, as ADFS is pretty environment specific (certs, server names, etc) it’s kinda tough to provide a complete sample solution – The code snippets in there are really just an http module (code file in the web project) that as I recall (its been a while) must be hooked up via web.config

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>