Passive Federation Client

3 ביוני 2013

no comments

As we all know it is simple to call a federated web site authenticated by AD FS 2.0 or any other identity provider using passive federation. The client is a browser that knows nothing about federation. All the browser knows is to send http requests and to submit html forms. It would be interesting to write a small library that will mimic the browser behavior and allow applications to call web sites using passive federation. such web sites can implement RESTful web services or any other http based API.

Currently applications use ACTIVE federation which means they have to know all about SAML and federation protocols. I wrote a nice library that allows application to call web sites using PASSIVE federation exactly like browser do.

It turns out to be quite easy.

When a browser calls a federated web site, the response will be 302 redirect, meaning that the request will be redetected to an STS. HttpWebRequest can automatically handle redirects if configured to do so by setting request.AllowAutoRedirect = true (the default)

The response of the STS after a successful authentication is a simple html form that contains the SAML token. The form should be submitted to send the SAML token to the RP (i.e. federated application).

Code Snippet
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <html>
  3.   <head>
  4.     <title>Working…</title>
  5.   </head>
  6.   <body>
  7.     <form method="POST" name="hiddenform" action="http://localhost:27858/">
  8.       <input type="hidden" name="wa" value="wsignin1.0" />
  9.       <input type="hidden" name="wresult" value="&lt;trust:RequestSecurityTokenResponseCollection xmlns:trust=&quot;http://docs.oasis-open.org/ws-sx/ws-trust/200512&quot;>&lt;trust:RequestSecurityTokenResponse Context=&quot;rm=0&amp;amp;id=passive&amp;amp;ru=%2f&quot;>&lt;trust:Lifetime>&lt;wsu:Created xmlns:wsu=&quot;http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd&quot;>2013-05-11T19:20:11.958Z&lt;/wsu:Created>&lt;wsu:Expires xmlns:wsu=&quot;http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd&quot;>2013-05-11T20:20:11.958Z&lt;/wsu:Expires>&lt;/trust:Lifetime>&lt;wsp:AppliesTo xmlns:wsp=&quot;http://schemas.xmlsoap.org/ws/2004/09/policy&quot;>&lt;wsa:EndpointReference xmlns:wsa=&quot;http://www.w3.org/2005/08/addressing&quot;>&lt;wsa:Address>http://localhost:27858/&lt;/wsa:Address>&lt;/wsa:EndpointReference>&lt;/wsp:AppliesTo>&lt;trust:RequestedSecurityToken>&lt;saml:Assertion MajorVersion=&quot;1&quot; MinorVersion=&quot;1&quot; AssertionID=&quot;_eef4ab5d-f5fc-441f-a395-40050adf293f&quot; Issuer=&quot;LocalSTS&quot; IssueInstant=&quot;2013-05-11T19:20:11.958Z&quot; xmlns:saml=&quot;urn:oasis:names:tc:SAML:1.0:assertion&quot;>&lt;saml:Conditions NotBefore=&quot;2013-05-11T19:20:11.958Z&quot; NotOnOrAfter=&quot;2013-05-11T20:20:11.958Z&quot;>&lt;saml:AudienceRestrictionCondition>&lt;saml:Audience>http://localhost:27858/&lt;/saml:Audience>&lt;/saml:AudienceRestrictionCondition>&lt;/saml:Conditions>&lt;saml:AttributeStatement>&lt;saml:Subject>&lt;saml:NameIdentifier>terry@contoso.com&lt;/saml:NameIdentifier>&lt;saml:SubjectConfirmation>&lt;saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer&lt;/saml:ConfirmationMethod>&lt;/saml:SubjectConfirmation>&lt;/saml:Subject>&lt;saml:Attribute AttributeName=&quot;name&quot; AttributeNamespace=&quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims&quot;>&lt;saml:AttributeValue>Terry&lt;/saml:AttributeValue>&lt;/saml:Attribute>&lt;saml:Attribute AttributeName=&quot;surname&quot; AttributeNamespace=&quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims&quot;>&lt;saml:AttributeValue>Adams&lt;/saml:AttributeValue>&lt;/saml:Attribute>&lt;saml:Attribute AttributeName=&quot;role&quot; AttributeNamespace=&quot;http://schemas.microsoft.com/ws/2008/06/identity/claims&quot;>&lt;saml:AttributeValue>developer&lt;/saml:AttributeValue>&lt;/saml:Attribute>&lt;saml:Attribute AttributeName=&quot;emailaddress&quot; AttributeNamespace=&quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims&quot;>&lt;saml:AttributeValue>terry@contoso.com&lt;/saml:AttributeValue>&lt;/saml:Attribute>&lt;saml:Attribute AttributeName=&quot;identityprovider&quot; AttributeNamespace=&quot;http://schemas.microsoft.com/accesscontrolservice/2010/07/claims&quot;>&lt;saml:AttributeValue>LocalSTS&lt;/saml:AttributeValue>&lt;/saml:Attribute>&lt;/saml:AttributeStatement>&lt;Signature xmlns=&quot;http://www.w3.org/2000/09/xmldsig#&quot;>&lt;SignedInfo>&lt;CanonicalizationMethod Algorithm=&quot;http://www.w3.org/2001/10/xml-exc-c14n#&quot; />&lt;SignatureMethod Algorithm=&quot;http://www.w3.org/2001/04/xmldsig-more#rsa-sha256&quot; />&lt;Reference URI=&quot;#_eef4ab5d-f5fc-441f-a395-40050adf293f&quot;>&lt;Transforms>&lt;Transform Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#enveloped-signature&quot; />&lt;Transform Algorithm=&quot;http://www.w3.org/2001/10/xml-exc-c14n#&quot; />&lt;/Transforms>&lt;DigestMethod Algorithm=&quot;http://www.w3.org/2001/04/xmlenc#sha256&quot; />&lt;DigestValue>7F9KaMoXg1nKb518dJNSqKjkSGNkzFw0ghvQGMQFbPA=&lt;/DigestValue>&lt;/Reference>&lt;/SignedInfo>&lt;SignatureValue>hoNXgWyIKUg8J4062X8sm/8iQOvGK8acQJVbqPTJmRhGGydglVLDWr/UIUiLPLgRDseAdiO26eG/+OMaKkiicwbeubPOZR5KEuNvSjX3yPqyF6NelagJHc99mHYDr18+doqGp/cQftO1+EFZSFedSZJcH4nrWK/oOSscTWMM9GY=&lt;/SignatureValue>&lt;KeyInfo>&lt;X509Data>&lt;X509Certificate>MIIB6zCCAVigAwIBAgIQi1q8MJwYe4xEYjoJne/rfTAJBgUrDgMCHQUAMBMxETAPBgNVBAMTCExvY2FsU1RTMB4XDTEyMDExMjIyMjA0MFoXDTM5MTIzMTIzNTk1OVowEzERMA8GA1UEAxMITG9jYWxTVFMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALClI6MgLV5EdnrZwcmsO+N8YoCgK4jWpncn2YxUU9cjCcYn1ks/9aSSJuFv9S3U6jalyN42jcCcP9/IIZngDMO0Rrdhyj+ra2AqP3Wj3oo6nHSpAmL+U32AZuxtvCLBsruik0OzKYkQshzdFLTthvIu7+jInanAmjn2T6Det8ehAgMBAAGjSDBGMEQGA1UdAQQ9MDuAEERfdZTmD4dsGFoHguIUVCahFTATMREwDwYDVQQDEwhMb2NhbFNUU4IQi1q8MJwYe4xEYjoJne/rfTAJBgUrDgMCHQUAA4GBAIdIbWFAVqq28keKyp6/UPOUxO3j2WsSxMm7yiePDhZVkaqLoq2QqySaHv3tvLA9GTRsd8E1RLSEZ7yZUVgv3J3n3GpD6RVcwxx9Dw1gEes7zZdq5KqnpgBqOEbUR1CEZa8hGswXbYN0Jve1+yqCObq1bfqcluHCWmhP9Fw9x1li&lt;/X509Certificate>&lt;/X509Data>&lt;/KeyInfo>&lt;/Signature>&lt;/saml:Assertion>&lt;/trust:RequestedSecurityToken>&lt;trust:RequestedAttachedReference>&lt;o:SecurityTokenReference k:TokenType=&quot;http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1&quot; xmlns:k=&quot;http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd&quot; xmlns:o=&quot;http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd&quot;>&lt;o:KeyIdentifier ValueType=&quot;http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID&quot;>_eef4ab5d-f5fc-441f-a395-40050adf293f&lt;/o:KeyIdentifier>&lt;/o:SecurityTokenReference>&lt;/trust:RequestedAttachedReference>&lt;trust:RequestedUnattachedReference>&lt;o:SecurityTokenReference k:TokenType=&quot;http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1&quot; xmlns:k=&quot;http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd&quot; xmlns:o=&quot;http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd&quot;>&lt;o:KeyIdentifier ValueType=&quot;http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID&quot;>_eef4ab5d-f5fc-441f-a395-40050adf293f&lt;/o:KeyIdentifier>&lt;/o:SecurityTokenReference>&lt;/trust:RequestedUnattachedReference>&lt;trust:TokenType>urn:oasis:names:tc:SAML:1.0:assertion&lt;/trust:TokenType>&lt;trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue&lt;/trust:RequestType>&lt;trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer&lt;/trust:KeyType>&lt;/trust:RequestSecurityTokenResponse>&lt;/trust:RequestSecurityTokenResponseCollection>" />
  10.       <input type="hidden" name="wctx" value="rm=0&amp;id=passive&amp;ru=%2f" />
  11.       <noscript>
  12.         <p>Script is disabled. Click Submit to continue.</p>
  13.         <input type="submit" value="Submit" />
  14.       </noscript>
  15.     </form>
  16.     <script language="javascript">window.setTimeout('document.forms[0].submit()', 0);</script>
  17.   </body>
  18. </html>

At the end of the form there is a simple javascript statement which submit the form:
<script language="javascript">window.setTimeout('document.forms[0].submit()', 0);</script>

To mimic the browser behavior all we need to do is parse the form and submit it. This is exactly what my simple library does.

Here is the code:

Code Snippet
  1. public class FederationHandler
  2.     {
  3.         public static HttpWebResponse SendFederationRequest(HttpWebRequest request, out string responseContent)
  4.         {
  5.             HttpWebResponse response;
  6.             
  7.             //The request with the saml token to send to the RP
  8.             HttpWebRequest request2 = null;
  9.  
  10.             try
  11.             {
  12.                 if (request == null)
  13.                     throw new NullReferenceException("request cannot be null");
  14.                 if (request.RequestUri == null)
  15.                     throw new NullReferenceException("request uri cannot be null");
  16.                 if (request.AllowAutoRedirect == false)
  17.                     throw new InvalidOperationException("AllowAutoRedirect must be true");
  18.  
  19.                 request.KeepAlive = true;
  20.  
  21.                 do
  22.                 {
  23.                     //send the request to the site
  24.                     if (request2 == null)
  25.                         response = (HttpWebResponse)request.GetResponse();
  26.                     else
  27.                         response = (HttpWebResponse)request2.GetResponse();
  28.                     
  29.                     using (StreamReader sr = new StreamReader(response.GetResponseStream()))
  30.                     {
  31.                         responseContent = sr.ReadToEnd();
  32.                     }
  33.  
  34.                     if (!response.ContentType.Contains("text")) //the SAML token form must be text/html or other textual content type.
  35.                         break;
  36.  
  37.                     if (!IsSamlTokenForm(responseContent))
  38.                         break;
  39.                 
  40.                     //Parse the token from the http form we got as a response
  41.                     var responseElement = XElement.Parse(responseContent);
  42.                     StringBuilder sb = new StringBuilder();
  43.  
  44.                     var action = responseElement.Descendants("form").First().Attribute("action").Value;
  45.                     foreach (var input in responseElement.Descendants("input"))
  46.                     {
  47.                         if (input.Attribute("type").Value != "submit")
  48.                         {
  49.                             sb.Append(input.Attribute("name").Value);
  50.                             sb.Append("=");
  51.                             sb.Append(HttpUtility.UrlEncode(input.Attribute("value").Value));
  52.                             sb.Append("&");
  53.                         }
  54.                     }
  55.                     var postString = sb.ToString();
  56.                     postString = postString.Remove(postString.Length – 1, 1); // remove the last '&'
  57.  
  58.                     //send the saml token in a POST request to the web-page
  59.                     request2 = (HttpWebRequest)WebRequest.Create(action);
  60.                     request2.Referer = response.ResponseUri.AbsoluteUri;
  61.                     request2.Method = "POST";
  62.                     request2.ContentLength = postString.Length;
  63.                     request2.ContentType = "application/x-www-form-urlencoded";
  64.                     request2.KeepAlive = true;
  65.                     request2.Headers.Add("Cache-Control", "max-age=0");
  66.                     request.CopyHeaders(request2);
  67.  
  68.                     // Read the result
  69.                     StreamWriter myWriter = null;
  70.                     try
  71.                     {
  72.                         myWriter = new StreamWriter(request2.GetRequestStream());
  73.                         myWriter.Write(postString);
  74.                     }
  75.                     finally
  76.                     {
  77.                         myWriter.Close();
  78.                     }
  79.  
  80.                 } while (true);
  81.  
  82.                 return response;
  83.  
  84.             }
  85.             catch (Exception ex)
  86.             {
  87.                 throw new InvalidOperationException("Could not establish federation request", ex);
  88.                 //TODO: LOG the exception
  89.             }
  90.         }
  91.  
  92.         /// <summary>
  93.         /// Verify that the string is a SAML token http form.
  94.         /// </summary>
  95.         /// <param name="responseString"></param>
  96.         /// <returns></returns>
  97.         private static bool IsSamlTokenForm(string responseString)
  98.         {
  99.             try
  100.             {
  101.                  var responseElement = XElement.Parse(responseString);
  102.                  var fields = responseElement.Descendants("input").ToList();
  103.                  if ((fields.Count == 4) || (fields.Count == 3))
  104.                  {
  105.                      if (fields.Count == 4) //SAML 1.1
  106.                          return ((fields[0].Attribute("name").Value == "wa") &&
  107.                                  (fields[1].Attribute("name").Value == "wresult") &&
  108.                                  (fields[2].Attribute("name").Value == "wctx"));
  109.                      if (fields.Count == 3) //SAML 2.0
  110.                          return ((fields[0].Attribute("name").Value == "SAMLResponse") &&
  111.                                  (fields[1].Attribute("name").Value == "RelayState"));
  112.                  }
  113.                 return false;
  114.  
  115.             }
  116.             catch (Exception)
  117.             {
  118.                 return false;
  119.             }
  120.         }
  121.     }

I created a helper method to copy http headers:

Code Snippet
  1. public static class Extensions
  2.     {
  3.         private static string[] speacialKeys = {"Authorization", "Accept", "Connection", "Content-Length", "Content-Type", "Date", "Expect", "Host", "If-Modified-Since", "Range", "Referer", "Transfer-Encoding", "User-Agent", "Proxy-Connection", "Cache-Control"};
  4.  
  5.         /// <summary>
  6.         /// Copy headers of one request to another
  7.         /// </summary>
  8.         /// <param name="source">The source request</param>
  9.         /// <param name="destination">he destination request</param>
  10.         public static void CopyHeaders(this HttpWebRequest source, HttpWebRequest destination)
  11.         {
  12.             if (!string.IsNullOrEmpty(source.Accept))
  13.                 destination.Accept = source.Accept;
  14.             if (!string.IsNullOrEmpty(source.Connection))
  15.                 destination.Connection = source.Connection;
  16.             if (source.ContentLength > 0)
  17.                 destination.ContentLength = source.ContentLength;
  18.             if (!string.IsNullOrEmpty(source.ContentType))
  19.                 destination.ContentType = source.ContentType;
  20.             if (!string.IsNullOrEmpty(source.Expect))
  21.                 destination.Expect = source.Expect;
  22.             if (!string.IsNullOrEmpty(source.TransferEncoding))
  23.                 destination.TransferEncoding = source.TransferEncoding;
  24.             if (!string.IsNullOrEmpty(source.UserAgent))
  25.                 destination.UserAgent = source.UserAgent;
  26.             if (!string.IsNullOrEmpty(source.MediaType))
  27.                 destination.MediaType = source.MediaType;
  28.             if (source.Date != default(DateTime))
  29.                 destination.Date = source.Date;
  30.             if (source.IfModifiedSince != default(DateTime))
  31.                 destination.IfModifiedSince = source.IfModifiedSince;
  32.             if (source.Proxy != null)
  33.                destination.Proxy = source.Proxy;
  34.             if (source.Timeout > 0)
  35.                destination.Timeout = source.Timeout;
  36.             if (source.ContinueDelegate != null)
  37.                 destination.ContinueDelegate = source.ContinueDelegate;
  38.             
  39.             destination.CookieContainer = source.CookieContainer ?? new CookieContainer();
  40.  
  41.            if (source.MaximumAutomaticRedirections > 0)
  42.                destination.MaximumAutomaticRedirections = source.MaximumAutomaticRedirections;
  43.             if (source.MaximumResponseHeadersLength > 0)
  44.                 destination.MaximumResponseHeadersLength = source.MaximumResponseHeadersLength;
  45.             if (source.ReadWriteTimeout > 0)
  46.                 destination.ReadWriteTimeout = source.ReadWriteTimeout;
  47.  
  48.             destination.AllowReadStreamBuffering = source.AllowReadStreamBuffering;
  49.             destination.AllowWriteStreamBuffering = source.AllowWriteStreamBuffering;
  50.             destination.AutomaticDecompression = source.AutomaticDecompression;
  51.             destination.ClientCertificates = source.ClientCertificates;
  52.             destination.SendChunked = source.SendChunked;
  53.             destination.Pipelined = source.Pipelined;
  54.             destination.ProtocolVersion = source.ProtocolVersion;
  55.             destination.UnsafeAuthenticatedConnectionSharing = source.UnsafeAuthenticatedConnectionSharing;
  56.  
  57.             foreach (var key in source.Headers.AllKeys)
  58.             {
  59.                 if (!speacialKeys.Contains(key))
  60.                     destination.Headers.Add(key, source.Headers.Get(key));
  61.             }
  62.  
  63.         }
  64.     }

Now we can call our federated web site in code:

Code Snippet
  1. static void test2()
  2.         {
  3.             string result;
  4.             var serviceAddress = @"http://manu-lap/SimpleWebApplication/";
  5.             HttpWebRequest request = (HttpWebRequest)WebRequest.Create(serviceAddress);
  6.             var response = FederationHandler.SendFederationRequest(request, out result);
  7.             Console.WriteLine(response.StatusCode.ToString());
  8.             Console.WriteLine(result);
  9.         }

Hope this helps

Manu

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*