Passing the RP realm identifier to an IP-STS from ADFS2 as a R-STS
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.
Thanks so much for this. really got us out of a bind.