Access to an ASP.NET website via multiple authentications

Background

Is it possible to secure a website using Windows Identity Foundation (WIF) without interfering with an existing authentication method? e.g. – Could a website secured using an ASP.NET membership provider, with all the code and configuration that entails, be layered with additional code and configuration to allow a precursory authentication with a trusted Identity Provider (IdP). In a more extreme case, would it be possible to force a user to authenticate with a trusted IdP of choice before also forcing them to authenticate with another trusted IdP? In both cases surely there would be too much overlap such that one would interfere with the other, making the combination untenable.

Well, before answering the question, why might you want to do this?

A recent project was concerned with providing Windows Azure hosting capabilities for customer websites. The product enables creation of customisable boiler plate websites and automated deployment and management/monitoring of said websites into Azure. As all websites in Azure are publicly available (ok you would have to know the cloudapp.net URL) the customer had no opportunity to test the finished website before it was available to a public audience. Therefore, a specific functional requirement was for the production website to be up and running in ‘preview’ mode and for only the preview audience to have access to it; the preview audience being a limited number of website developers/stakeholders. Once the website passed muster, a switch could be flicked and the public audience would gain access.

(It should be mentioned that securing the site by client ip address was untenable as the preview audience could range from tablet owners sitting in internet cafes to desktop based developers working either at home or in the office.)

Instead, the customer exposes their corporate Active Directory to the internet using Active Directory Federation Services (ADFS 2.0) and so this was potentially an authoritative source for authenticating the preview audience. The problem was how to use this resource without fundamentally modifying the existing authentication mechanism for the website.

Essentially:

“I want authN method X in production but in preview I want authN method Y followed by authN method X”

So, in the production configuration (i.e – authN method X only), the website may utilise an ASP.NET membership provider or perhaps federate with an external identity provider such as Live, Facebook etc. In fact it could employ any flavour of authentication/authorisation. An example is shown below where WIF is used to federate with Live, via the Azure Access Control Service (ACS):

image

In the preview configuration, there would be an additional authentication step so that the preview audience would have to log in with their corporate credentials against ADFS 2.0 and then also have to log in to Live, via ACS. An example is shown below:

image

In the methodology that follows I have used Visual Studio 2012, .NET4.5 (WIF is baked into .NET4.5 whereas previously it was a separate download), IIS8 and Windows Server 2012. However, the general methodology will also apply to Visual Studio 2010, .NET4, IIS7.5 and Windows Server 2008 R2.

Methodology

Ordinarily, WIF is enforced by two HTTP modules which are injected into the web.config file for the website (I have abbreviated the assembly names for formatting purposes):

 <system.webServer>
  <modules>
    <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services" />
    <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services" />
  </modules>
</system.webServer>

These modules implement event handlers that are raised in the ASP.NET pipeline for each HTTP request. Using the source code decompiler of your choice, we find that the earliest event handler for each module is the AuthenticateRequest event.

To layer ADFS authentication on top of this I am going to implement a custom HTTP module and similarly handle the AuthenticateRequest event. Crucially though, the custom module is first in the module list so that it fires before the WIF modules:

 <system.webServer>
  <modules>
    <add name="BouncerModule" type="RelyingParty.BouncerModule, RelyingParty" />
    <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services" />
    <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services" />
  </modules>
</system.webServer>

Apologies for the name ‘BouncerModule’. It just kind of stuck amongst the team and it does sort of act in the role of a nightclub doorman.

So, what are the important characteristics of the module?

  • It must enforce federation with an identity provider
    • In this case the corporate ADFS
  • It should make use of WIF classes as much as possible to avoid reinventing the wheel
  • It must not interfere with the existing ASP.NET pipeline for the website
    • None of the configuration contained within the web.config file for the website can be repurposed. e.g. -
      • <system.web>/<authorization>
      • <system.web>/<authentication>
    • The module should not set anything in the HTTP context. CurrentPrincipal etc.

To achieve most of these aims I will make use of named WIF configuration.

What do I mean by named configuration?

For a standard WIF protected website in .NET 4.5 (in this case the website federates with Azure Access Control Service (ACS)), the configuration looks like the following:

 <system.identityModel>
  <identityConfiguration>
    <audienceUris>
      <add value="urn:relyingpartyacs.com" />
    </audienceUris>
    <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel">
      <trustedIssuers>
        <add thumbprint="4AC4CB9A11C9EEDA95DA3B65295BD56F365E65FF" name="https://bradcot.accesscontrol.windows.net/" />
      </trustedIssuers>
    </issuerNameRegistry>
    <certificateValidation certificateValidationMode="None" />
  </identityConfiguration>
</system.identityModel>
<system.identityModel.services>
  <federationConfiguration>
    <cookieHandler requireSsl="true" />
    <wsFederation passiveRedirectEnabled="true" issuer="https://bradcot.accesscontrol.windows.net/v2/wsfederation" realm="urn:relyingpartyacs.com" requireHttps="true" />
  </federationConfiguration>
</system.identityModel.services>

For this configuration, when the website is browsed, the user is asked to authenticate at Live, via Azure ACS, as is desired in production.

Below the configuration is extended to include sections that are explicitly named. A new <identityConfiguration> element named ‘adfs’ is added (the name chosen is not important) which in turn is referenced by a similarly named <federationConfiguration> element.

 <system.identityModel>
  <identityConfiguration>
    <audienceUris>
      <add value="urn:relyingpartyacs.com" />
    </audienceUris>
    <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel">
      <trustedIssuers>
        <add thumbprint="4AC4CB9A11C9EEDA95DA3B65295BD56F365E65FF" name="https://bradcot.accesscontrol.windows.net/" />
      </trustedIssuers>
    </issuerNameRegistry>
    <certificateValidation certificateValidationMode="None" />
  </identityConfiguration>
   <identityConfiguration name="adfs">    <audienceUris>      <add value="urn:relyingpartyadfs.com" />    </audienceUris>    <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel">      <trustedIssuers>        <add thumbprint="416565b7d2193df7cc82de7981df59a600b7af67" name="https://www.bradsts.com/adfs/services/trust/" />      </trustedIssuers>    </issuerNameRegistry>    <certificateValidation certificateValidationMode="None" />  </identityConfiguration> 










</system.identityModel>
<system.identityModel.services>
  <federationConfiguration>
    <cookieHandler requireSsl="true" />
    <wsFederation passiveRedirectEnabled="true" issuer="https://bradcot.accesscontrol.windows.net/v2/wsfederation" realm="urn:relyingpartyacs.com" requireHttps="true" />
  </federationConfiguration>
   <federationConfiguration name="adfs" identityConfigurationName="adfs">    <cookieHandler requireSsl="true" />    <wsFederation passiveRedirectEnabled="true" issuer="https://www.bradsts.com/adfs/ls/" realm="urn:relyingpartyadfs.com" requireHttps="true" />  </federationConfiguration> 



</system.identityModel.services>

The WIF pipeline will only use the unnamed sections. However, with the bouncer module in place, the named configuration can be used as follows:

 

image

N.B. – I have not included the code but would be happy to discuss any aspect of it if you have specific questions.

Some steps need a bit of explanation:

4 & 12 – It is crucial that the session cookie created by the bouncer module has a different name to that used by the WIF pipeline, ‘FedAuth’. In this way the ADFS token that is contained within the cookie does not overlap with the ACS token.

6 – If the ADFS token passes validation there is nothing more to be done. End and allow the WIF pipeline to process the session cookie containing the ACS token. Otherwise, redirect the browser to ADFS for re-authentication.

11 – The reply address is stored within the ‘ru’ query string parameter of the redirect received from ADFS. The reply address is either the root of the website or a relative URI to a location within the website. If these checks are not performed it might be possible for a nefarious query string to cause the browser to be redirected to a malicious website.

Conclusion

In this article I have shown that it is possible to add an extra authentication step into a website, on top of the existing authentication method. In the example a website which, in production, would federate with Live via Azure ACS, was altered so that a precursory authentication against ADFS was mandated.

This was all achieved using named WIF configuration and a standalone ASP.NET HTTP module that very deliberately leaves no trace in the pipeline after execution is complete.

[BradleyCotier]