Project Description
How to enable ADFS to select an authentication method based on the service that the user is accessing.

Problem

Fujitsu Services have implemented ADFS solutions for a number of customers. One customer recently asked if it was possible for ADFS to use Forms Based Authentication for one of the service providers (aka relaying party) and use Integrated Windows Authentication for all other service providers.

The customer was worried that when a user leaves their desk unattended, and another user could use the unattended laptop to access a site containing sensitive information without being challenged. For the sensitive site they required the users to be challenged with the ADFS sign in page. But they want to contain using IWA for less sensitive sites.

Background

Out-of-the-box, an ADFS instance uses the same authentication method for all service providers. The only exceptions are (1) using a ADFS proxy in which case the authentication method depends on the location of the users, or (2) the AuthnRequest from the service provider specifies the authentication method it requires ADFS to use.

In this case the location of the user is not relevant. And the service provider was unable to alter their AuthnRequest requiring FBA to be used. Even if this was possible it would only work for Service Provider Initiated Single Sign On. Identity Provider Initiated Single Sign On would revert to ADFS default authentication method.

Alternatives

A simple alternative is to implement a second ADFS instance. The original ADFS could continue to use IWA and trust the existing service providers. The second ADFS would use FBA and trust the new service provider. But the customer did not want to support additional servers.

A second alternative is to edit ADFS IdpInitiatedSignOn.aspx.cs to generate a AuthnRequest specifying the use of FBA is the relying if the value of the QueryString parameter LoginToRp matches the service provider's URI. This would work for IdP Initiated SSO but not for SP Initiated SSO.

Solution

To support both IdP Initiated SSO and SP Initiated SSO we need to edit a web page that is used regardless of how SSO is initiated. There is no such page, unless we make FBA the default authentication method. Then, we can edit FormsSignIn.aspx.cs to identify the relying party and choose to remain on the current page or redirect to an alternative page that supports an alternative authentication method.

Finally, SSO across the service providers must be switched off. Otherwise users could access the least sensitive site where they are authenticated using IWA and then access the more sensitive site without having to authenticate via FBA. Disabling SSO does not affect the end-users' experience when accessing the sites supported by IWA. But it ensure the user must sign on via FBA everytime they access a sensitive site.

Change ADFS web.config

In ADFS's web.config (C:\inetpub\adfs\ls) you need to ensure the first element in localAuthenticationTypes is the one for Forms; and you need to change singleSignOn's enabled attribute to false.
<?xml version="1.0"?>
<configuration>
  ...
  <microsoft.identityServer.web>
    <localAuthenticationTypes>
      <add name="Forms" page="FormsSignIn.aspx" />
      <add name="Integrated" page="auth/integrated/" />
      <add name="TlsClient" page="auth/sslclient/" />
      <add name="Basic" page="auth/basic/" />
    </localAuthenticationTypes>
    ...
    <singleSignOn enabled="false" />
  </microsoft.identityServer.web>
  ...
</configuration>

Change FormsSignIn.aspx.cs

Add the namespaces to the file.

using System.Configuration;
using System.Collections.Specialized;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Xml;

Add the method GetRelyingPartnerUrl. This method identifies the URL of the service provider. It handles IdP Initated SSO and SP Initiated SSO (using WS-Federation, SAML Redirect Binding or SAML POST Binding).

    private string GetRelyingPartnerUrl()
    {
        // If using WS-Federation, get issuer from wtrealm query parameter
        NameValueCollection queryParams = System.Web.HttpUtility.ParseQueryString(Request.Url.Query);
        string wtrealm = queryParams["wtrealm"];
        if (!String.IsNullOrEmpty(wtrealm))
        {
            return wtrealm;
        }

        // Next try to get SAML Request for the URL (i.e. user is using IdP Initiated SSO or 
        // SAML SSO Redirect binding).  The SAML Request is Base64 encoded and compressed
        string decodedSamlRequest;
        string urlSamlRequestBase64 = queryParams["SAMLRequest"];

        if (!String.IsNullOrEmpty(urlSamlRequestBase64))
        {
            // convert from Base64
            byte[] encodedCompressedDataAsBytes = Convert.FromBase64String(urlSamlRequestBase64);
            using (MemoryStream inputBuffer = new MemoryStream())
            {
                // Read the data via a deflate stream input an output buffer
                using (MemoryStream outputBuffer = new MemoryStream(encodedCompressedDataAsBytes))
                {
                    using (DeflateStream inflateStream = new DeflateStream(outputBuffer, CompressionMode.Decompress, false))
                    {
                        // Should really be able to use inflateStream.CopyTo(inputBuffer) but it is throwing an exception
                        int c;
                        while ((c = inflateStream.ReadByte()) >= 0)
                        {
                            inputBuffer.WriteByte((byte)c);
                        }
                    }
                }
                // Write the output (XML) to a string
                decodedSamlRequest = Encoding.UTF8.GetString(inputBuffer.ToArray());
            }
        }
        // Next try to get SAML Request from FORMS (i.e. user is using SAML SSO POST Binding)
        // It's Base64 encoded, and ADFS embeddes it in MSISSamlRequest
        else
        {
            // Get Base54 encoded MSI SAML Request from Request parameters
            string msiSamlRequestParam = Request.Params["MSISSamlRequest"];
            if (String.IsNullOrEmpty(msiSamlRequestParam))
            {
                return null;
            }
            string[] msiSamlRequestList = msiSamlRequestParam.Split(',');
            string msiSamlRequestBase64 = msiSamlRequestList[msiSamlRequestList.Length - 1];

            // Decode MSI SAML Request parameter
            byte[] encodedDataAsBytes = System.Convert.FromBase64String(msiSamlRequestBase64);
            string msiSamlRequest = Encoding.UTF8.GetString(encodedDataAsBytes);

            // Get Base64 encoded SAML Request from the decoded MSI SAML Request
            int startIndex = msiSamlRequest.IndexOf("SAMLRequest=") + 12;
            if (startIndex == -1)
            {
                return null;
            }
            string samlRequestBase64;
            int endIndex = msiSamlRequest.IndexOf("\\", startIndex);
            if (endIndex == -1)
            {
                samlRequestBase64 = msiSamlRequest.Substring(startIndex);
            }
            else
            {
                samlRequestBase64 = msiSamlRequest.Substring(startIndex, endIndex - startIndex);
            }
            samlRequestBase64 = samlRequestBase64.Replace("%3d", "=").Replace("%2b", "+"); //TODO better way?

            // Decode SAML Request parameter
            encodedDataAsBytes = System.Convert.FromBase64String(samlRequestBase64);
            decodedSamlRequest = Encoding.UTF8.GetString(encodedDataAsBytes);
        }

        // Failed to get a SAML Request - Should not happen
        if (String.IsNullOrEmpty(decodedSamlRequest))
        {
            return null;
        }

        // Get the URL of service provider from the SAML Request
        XmlDocument doc = new XmlDocument();
        doc.LoadXml(decodedSamlRequest);

        // Look at Audience, incase the issuer is ADFS
        XmlNodeList audienceList = doc.GetElementsByTagName("Audience","urn:oasis:names:tc:SAML:2.0:assertion");
        if ((audienceList.Count > 0) &&
            (audienceList[0] is XmlElement))
        {
            return ((XmlElement)audienceList[0]).InnerText;
        }

        // If Audience is not used, look at Issuer
        XmlNodeList issuerList = doc.GetElementsByTagName("Issuer","urn:oasis:names:tc:SAML:2.0:assertion");
        if ((issuerList.Count > 0) &&
            (issuerList[0] is XmlElement))
        {
            return ((XmlElement)issuerList[0]).InnerText;
        }

        return null;
    }

Edit the PageLoad method with your logic to map Relying Party URL to authentication method. In the example below, I compare the relying partner's URL against a comma separated lists of URLs stored in the appSettings fbaRelyingPartners. If a match is found, PageLoad exits leaving the user on sign in page. If a match is not found, the user is redirected to the URL for IWA. If you choose to copy this Page_Load, remember to add fbaRelyingPartners to appSettings in ADFS' web.config.
    protected void Page_Load( object sender, EventArgs e )
    {
          string rpIdentity = GetRelyingPartnerUrl();

          if (!String.IsNullOrEmpty(rpIdentity))
          {
              // Is the replying partner is a service provider who's authentication method should be FBA
              Uri rpUri;
              if (Uri.TryCreate(rpIdentity, UriKind.Absolute, out rpUri))
              {
                  string rpHost = rpUri.Host;

                  NameValueCollection appSettings = ConfigurationManager.AppSettings;
                  string fbaRelyingPartners = appSettings["fbaRelyingPartners"];
                  if (!String.IsNullOrEmpty(fbaRelyingPartners))
                  {
                      foreach (string fbaRelyingPartner in fbaRelyingPartners.Split(','))
                      {
                          if (fbaRelyingPartner.Equals(rpHost, StringComparison.InvariantCultureIgnoreCase))
                          {
                              return;
                          }
                      }
                  }
              }
          }

          // Else redirect for IWA
          Response.Redirect("/adfs/ls/auth/integrated/" + Request.Url.Query);
    }

Last edited Jul 23, 2012 at 4:04 PM by slang, version 10