SAML 2.0 tokens and WIF – bridging the divide

Background

We all know the following limitations about Windows Identity Foundation (WIF) and passive (browser) federation protocols, right?

  • WIF does not support SAML2.0 protocol (SAML2P)
    • There is a WIF extension out there to support SAML2P but it is a technology preview
  • WIF does support SAML2.0 (SAML2) tokens
  • WS-Federation conveys SAML1.1 tokens

Therefore, unless you use the WIF extension there is no way of getting a SAML2 token into a Relying Party (RP) application.

Well, did you know the following?

  • WS-Federation responses convey WS-Trust RequestSecurityTokenResponse (RSTR) elements containing security tokens
  • The RSTR element is not tied into a particular token format
    • i.e. – it is a <xs:any> element in the schema

This means that it is perfectly valid for a WS-Federation response to convey a SAML2.0 token and it is only because WS-Federation existed before SAML2P that it became a de facto convention for WS-Federation to convey SAML1.1 tokens.

So, it IS possible to get a SAML2 token into a RP application as long as it is possible to get a SAML2 token into a WS-Federation response.

Here’s how.

Methodology

To achieve this I am using Active Directory Federation Services (ADFS 2.0) as it is simple to integrate with WIF and it understands SAML2P, and therefore SAML2 tokens.

The setup is a Windows Server 2012 domain controller with the ADFS 2.1 role installed. All code was written using Visual Studio 2012 and .NET framework 4.5 although the same could be achieved with Windows Server 2008 R2, Visual Studio 2010 and .NET framework 4.0.

As discussed, this won’t work:

image

But this should:

image

The strategy is to place an intermediary Relying Party Security Token Service (RP-STS) between RP and ADFS which performs protocol transition from SAML2P to WS-Federation.

To make things a little simpler, I have applied the following restrictions to the RP-STS:

  • It is not a signing authority
    • That is, it cannot create authoritative tokens itself but simply passes on tokens received from other providers
  • It does not validate either the protocol wrapper or token itself
    • This would involve a lot of extra code and the RP is going to do it anyway
    • The SAMLResponse wrapper element received from ADFS containing a SAML2 token is not digitally signed
      • The SAML2P specification does allow for a digital signature though
  • The RP-STS only recognises messages transmitted using the SAML 2.0 HTTP redirect and POST bindings

The RP-STS itself is simply a ASP.NET HTTP module hosted in IIS. The first event in the ASP.NET pipeline, BeginRequest, is handled as there is nothing more for the module to do than parse tokens.

The flow of events is as follows:

image

The actual code is shown below:

 namespace RelyingPartySTS
{
    using System;
    using System.Configuration;
    using System.IdentityModel.Protocols.WSTrust;
    using System.IdentityModel.Services;
    using System.IdentityModel.Tokens;
    using System.IO;
    using System.Web;
    using System.Xml;

    public class TranslatorModule : IHttpModule
    {
        private const string SamlRequestTemplate = "<samlp:AuthnRequest ID=\"id-{0}\" Version=\"2.0\" IssueInstant=\"{1}\" Destination=\"https://www.bradsts.com/adfs/ls/\" Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">{2}</Issuer><samlp:NameIDPolicy Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\" AllowCreate=\"true\" /></samlp:AuthnRequest>";

        private const string SamlHandlerContent = "<html><head><title>Working...</title></head><body><form method='POST' name='hiddenform' action='{0}'><input type='hidden' name='SAMLRequest' value='{1}' /><noscript><p>Script is disabled. Click Submit to continue.</p><input type='submit' value='Submit' /></noscript></form><script language='javascript'>window.setTimeout('document.forms[0].submit()', 0);</script></body></html>";

        public void Dispose()
        {
            //clean-up code here.
        }

        public void Init(HttpApplication context)
        {
            context.BeginRequest += new EventHandler(OnBeginRequest);
        }

        public void OnBeginRequest(Object source, EventArgs e)
        {
            try
            {
                WSFederationMessage mess = WSFederationMessage.CreateFromUri(HttpContext.Current.Request.Url);

                if (mess is SignInRequestMessage)
                {
                    SignInRequestMessage messIn = mess as SignInRequestMessage;

                    // Convert the WS-Fed request into a SAML2 request
                    string parsedRequest = System.Web.HttpUtility.HtmlEncode(
                        System.Convert.ToBase64String(
                        System.Text.Encoding.UTF8.GetBytes(
                        string.Format(
                            SamlRequestTemplate, 
                            Guid.NewGuid().ToString(), 
                            DateTime.UtcNow.ToString("yyyy-MM-ddTHH:MM:ss.fffZ"), 
                            messIn.Realm))));

                    // Redirect the request to ADFS
                    string finalisedRedirectString = string.Format(
                        SamlHandlerContent,
                        ConfigurationManager.AppSettings["stsUrl"],
                        parsedRequest);

                    HttpContext.Current.Response.Write(finalisedRedirectString);
                    HttpContext.Current.ApplicationInstance.CompleteRequest();
                }
            }
            catch (WSFederationMessageException)
            {
                // Parse the SAMLResponse
                if (HttpContext.Current.Request.Form.HasKeys())
                {
                    if (HttpContext.Current.Request.Form["SAMLResponse"] != null)
                    {
                        // Decode the response
                        string samlResponse = HttpContext.Current.Request.Form["SAMLResponse"];
                        string responseDecoded = System.Text.Encoding.UTF8.GetString(System.Convert.FromBase64String(System.Web.HttpUtility.HtmlDecode(samlResponse)));

                        Saml2SecurityToken token = null;

                        // Pick out the token
                        using (StringReader sr = new StringReader(responseDecoded))
                        {
                            using (XmlReader reader = XmlReader.Create(sr))
                            {
                                reader.ReadToFollowing("Assertion", "urn:oasis:names:tc:SAML:2.0:assertion");
                                
                                // Deserialize the token so that data can be taken from it and plugged into the RSTR
                                SecurityTokenHandlerCollection coll = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
                                token = (Saml2SecurityToken)coll.ReadToken(reader.ReadSubtree());
                            }
                        }

                        // Create a WS-Fed sign in response
                        RequestSecurityTokenResponse rstr = new RequestSecurityTokenResponse();
                        rstr.TokenType = "urn:oasis:names:tc:SAML:2.0:assertion";
                        rstr.RequestType = "https://schemas.xmlsoap.org/ws/2005/02/trust/Issue";
                        rstr.KeyType = "https://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey";
                        rstr.Lifetime = new Lifetime(token.Assertion.IssueInstant, token.Assertion.Conditions.NotOnOrAfter);
                        rstr.AppliesTo = new EndpointReference(token.Assertion.Conditions.AudienceRestrictions[0].Audiences[0].OriginalString);
                        rstr.RequestedSecurityToken = new RequestedSecurityToken(token);
                        WSTrustFeb2005ResponseSerializer ser = new WSTrustFeb2005ResponseSerializer();
                        MemoryStream ms = new MemoryStream();

                        SignInResponseMessage messOut = null;

                        using (StreamReader sr = new StreamReader(ms))
                        {
                            using (XmlWriter writer = XmlWriter.Create(ms))
                            {
                                ser.WriteXml(rstr, writer, new WSTrustSerializationContext());
                            }

                            ms.Position = 0;

                            messOut = new SignInResponseMessage(new Uri(ConfigurationManager.AppSettings["rpUrl"]), sr.ReadToEnd());
                        }                    

                        // Redirect the browser to RP
                        messOut.WriteFormPost();
                        HttpContext.Current.Response.Write(messOut.WriteFormPost());
                        HttpContext.Current.ApplicationInstance.CompleteRequest();
                    }
                }
            }
        }
    }
}

This ‘almost’ works in that the SAML2 token is successfully extracted from the ADFS response and plugged into a WS-Federation response which is forwarded to the RP website.

However, it turns out that WIF takes exception to the token! You will get the following error message:

“ID4154: A Saml2SecurityToken cannot be created from the Saml2Assertion because it contains a SubjectConfirmationData which specifies an InResponseTo value. Enforcement of this value is not supported by default. To customize SubjectConfirmationData processing, extend Saml2SecurityTokenHandler and override ValidateConfirmationData.”

To avoid this error, do the following:

  • Create a custom SecurityTokenHandler implementation derives from the default Saml2SecurityTokenHandler
  • Override the ValidateConfirmationData method
  • Remove the default Saml2SecurityTokenHandler in the <system.identityModel> config section
  • Add in the new implementation

An aside:

In fact, upon inspection of the SAML2 token, the InResponseTo attribute referred to above is actually inappropriate for the RP website as it contains the URL for the RP-STS website. However, as WIF by default block tokens containing this attribute, there probably isn’t much code that would care about the value contained within the attribute. That said, this warrants careful consideration if you are using this template for a production scenario.

I am pretty sure that this inaccuracy in the URL could be circumvented by combining the RP and RP-STS websites together so that they exist under a common domain name (I will investigate this and post my findings). The conversion from WS-Federation request to SAMLRequest could take place in the OnRedirectingToIdentityProvider event handler and conversely, the conversion from ADFS SAMLResponse to WS-Federation response could take place in a HTTP module which fires before the default WIF pipeline takes over.

Conclusion

I have shown that it is possible to use relatively few lines of code to facilitate the transmission of SAML2 tokens into a standard WIF protected website by transitioning to/from WS-Federation to SAML2P federation protocols.

I believe that in the absence of proper SAML2P support in Windows Identity Foundation this approach, with a bit of work to bring the code up to production standards, is a viable one.

Bradley Cotier