April 17, 2015 // By Magenic
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…
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 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:
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 post was republished from Magenic Solutions Architect Dave Stienessen’s blog, The Pragmatic Architect, and can be found here.
If you would rather speak to us directly, please go to our contact page or call us at 877-277-1044.