Skip to content

Passing the RP realm identifier to an IP-STS from ADFS2 as a R-STS

May 29, 2012

Wow, that’s an awful title. Oh well, here we go:

When ADFS2 is being used as a R-STS for protocol transition (SAML2-P to WS-Fed, for example) the IP-STS is not aware of the original RP requesting the token. From the IP-STS’ perspective it only knows the immediate RP (which is really ADFS2 acting as a R-STS):

Sometimes it would be nice for the the IP-STS to know what the original RP was requesting the token. This would be useful for branding or tailoring claims issuance. I had a client that needed to do exactly this (he was using IdentityServer as the IP-STS), so I had to figure out how to pass along an additional parameter to IdSrv from ADFS2 to indicate the original realm requesting the token (for both for WS-Fed and SAML2-P requests). Note: doing this was very non-standard but my client was alright with this since ADFS2 was only being used as a bridge from a SAML2-P RP to WS-Fed. If WIF supported SAML2-P then using ADFS2 in the middle would not be necessary and the IP-STS would have know the realm for the RP. So yea, this is a hack/workaround. Oh well.

Anyway, in the end it was quite simple and the approach taken was mainly due to the fact that most of the code surrounding the redirect from ADFS2 to the IP was buried in the ADFS2 base classes so I was unable to modify the URL for the redirect via the ADFS2 APIs. I ended up hooking into the ASP.NET pipeline which is hosting ADFS2. What I had to do was: 1) Know how to extract the realm for the RST and 2) Know how to add the realm to the query string when ADFS2 was redirecting to the IP-STS.

For #1: Checking the RST for WS-Fed is easy — if there’s a Request.QueryString[“wtrealm”] then you’re done. But the SAML2-P RST query string param is more complicated. I had to deal with decoding, decompressing and XML-ifying the query string param (according to the SAML2-P spec), but once that was all done the RP identifier was in there.

For #2: Hooking into and modifying ADFS2’s redirect was just a matter of handling the Application_EndRequest in ASP.NET and checking for a 302 HTTP status code on the Response object. Once I know ADFS2 is redirecting, I can just extract the realm from the original query string and then do my own redirect appending the custom query string param for the IP-STS.

I won’t bother describing the rest of the details since the code can speak for itself. Here’s the code I added to global.asax in ADFS2:

void Application_EndRequest()
{
    CheckForRSTRealmAndRedirect();
}

void CheckForRSTRealmAndRedirect()
{
    if (Response.StatusCode == 302)
    {
        string realm = GetRPRealmFromUrl();
        if (realm != null)
        {
            string redirectUrl = Response.RedirectLocation;
            Response.Redirect(redirectUrl + "&rp-realm=" + realm);
        }
    }
}

string GetRPRealmFromUrl()
{
    try
    {
        string samlParam = Request.QueryString["SAMLRequest"];
        if (samlParam != null)
        {
            string urlDecodedParam = HttpUtility.UrlDecode(samlParam);
            var realm = ExtractRealmFromSamlRequest(urlDecodedParam);
            return realm;
        }
        else
        {
            string realm = Request.QueryString["wtrealm"];
            return realm;
        }
    }
    catch (Exception)
    {
    }
    return null;
}

string ExtractRealmFromSamlRequest(string urlDecodedParam)
{
    byte[] bytes = Convert.FromBase64String(urlDecodedParam);
    using (MemoryStream ms = new MemoryStream(bytes))
    {
        byte[] b = DecompressDeflate(ms);
        if (b == null) return null;

        string deflatedString = Encoding.ASCII.GetString(b);
        XmlDocument doc = new XmlDocument();
        doc.LoadXml(deflatedString);

        XmlNamespaceManager ns = new XmlNamespaceManager(doc.NameTable);
        ns.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion");
        XmlNode node = doc.SelectSingleNode("//saml:Issuer", ns);
        return node.InnerText;
    }
}

byte[] DecompressDeflate(Stream streamInput)
{
    using (Stream streamOutput = new MemoryStream())
    {
        int bytesRead = 0;

        try
        {
            byte[] readBuffer = new byte[4096];
            using (DeflateStream stream = new DeflateStream(streamInput, CompressionMode.Decompress))
            {
                int i;
                while ((i = stream.Read(readBuffer, 0, readBuffer.Length)) != 0)
                {
                    streamOutput.Write(readBuffer, 0, i);
                    bytesRead = bytesRead + i;
                }
            }
        }
        catch (Exception)
        {
            return null;
        }

        byte[] buffer = new byte[bytesRead];
        streamOutput.Position = 0;
        streamOutput.Read(buffer, 0, buffer.Length);
        return buffer;
    }
}

In the IP-STS’ login page it’s just a matter of looking for the “rp-realm” query string param:

public ActionResult SignIn(string returnUrl)
{
    var url = HttpUtility.UrlDecode(returnUrl);
    var marker = "rp-realm=";
    var idx = url.IndexOf(marker);
    if (idx >= 0)
    {
        idx += marker.Length;
        var idx2 = url.IndexOf('&', idx);
        if (idx2 < 0) idx2 = url.Length - idx;
        var rp_realm = url.Substring(idx, idx2);
    }

    ViewBag.ReturnUrl = returnUrl;
    ViewBag.ShowClientCertificateLink = ConfigurationRepository.Configuration.EnableClientCertificates;

    return View();
}

Presumably the IP-STS would then use rp-realm to perform any branding or customization necessary.

One Comment leave one →
  1. channa permalink
    February 28, 2014 11:02 am

    Thanks so much for this. really got us out of a bind.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: