How to get a SAML Protocol Response from ADFS using C#

ADFS (Active Directory Federation Services) is a fancy name for Windows Identity Foundation Server. ADFS supports SAML protocol, however its client, Windows Identity Foundation (WIF), does not. As most of the problems of acquiring a token can be resolved with either WS-Federation and WS-Trust, you may use WIF for your federation needs since WIF supports SAML-Token (please notice SAML-Protocol is not the same as SAML-Token).

Lately I have received requests from partners and customers to acquire a SAML Protocol Response which would require SAML-Protocol to request. WIF unfortunately cannot be used to make a SAML-Protocol request and there is no out-of-the-box way of doing that. There are some paid NuGets implementing SAML-Protocol in C#, but none is free. I put together a workaround to request a SAML-Protocol response from ADFS in C# using HttpClient (from System.Net.Http library). System.Net.Http.HttpClient class comes with .NET 4.5 and 4.5.1 and can be added via NuGet to .NET 4.0. The idea is to leverage ADFS Idp Initiated Login page and follow the redirects to extract the SAML Response that happens during the process of logging in to a SAML-Protocol endpoint.

Requirements

You have to create a SAML-Protocol Assertion Endpoint with POST binding in your reliant party configuration. This endpoint can co-exist with a Federation endpoint.

image

Code

I put together two samples. One that shows step-by-step using a Windows Forms application how to acquire a SAML Response (you can download it here):

image

Windows Forms Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Data;
  5. using System.Drawing;
  6. using System.Linq;
  7. using System.Net.Http;
  8. using System.Text;
  9. using System.Threading.Tasks;
  10. using System.Windows.Forms;
  11. using System.Xml;
  12. using System.Text.RegularExpressions;
  13. using System.Web;
  14. using System.IO;
  15. using System.IO.Compression;
  16. //The code samples are provided AS IS without warranty of any kind.
  17. // Microsoft disclaims all implied warranties including, without limitation,
  18. // any implied warranties of merchantability or of fitness for a particular purpose.
  19. /*
  20. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you.
  21. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts
  22. be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption,
  23. loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts
  24. or documentation, even if Microsoft has been advised of the possibility of such damages.
  25. */
  26. namespace GetSaml
  27. {
  28.     public partial class Form1 : Form
  29.     {
  30.         public Form1()
  31.         {
  32.             InitializeComponent();
  33.         }
  34.         private string getUrl = null;
  35.         private string GetUrl
  36.         {
  37.             get
  38.             {
  39.                 if (!String.IsNullOrEmpty(getUrl))
  40.                     return getUrl;
  41.                 StringBuilder domain = new StringBuilder();
  42.                 domain.Append(Environment.GetEnvironmentVariable(“USERDNSDOMAIN”));
  43.                 if (domain.Length > 0)
  44.                 {
  45.                     domain.Clear();
  46.                     domain.Append(Environment.UserDomainName);
  47.                 }
  48.                 
  49.                 //return String.Format(“https://{0}.{1}.lab/adfs/ls/MyIdpInitiatedSignOn.aspx?loginToRp=https://mysyte.com”, Environment.MachineName.ToLower(), Environment.UserDomainName.ToLower());
  50.                 return String.Format(“https://{0}.{1}.lab/adfs/ls/IdpInitiatedSignOn.aspx”, Environment.MachineName.ToLower(), Environment.UserDomainName.ToLower());
  51.             }
  52.         }
  53.         private void Form1_Load(object sender, EventArgs e)
  54.         {
  55.             textBox1.Text = GetUrl;
  56.         }
  57.         protected List<KeyValuePair<string, string>> forms;
  58.         private HttpClient client = null;
  59.         private HttpClientHandler handler;
  60.         private HttpClient Client
  61.         {
  62.             get
  63.             {
  64.                 if (client == null)
  65.                 {
  66.                     handler = new HttpClientHandler();
  67.                     handler.UseDefaultCredentials = true;
  68.                     handler.AllowAutoRedirect = false;
  69.                     handler.CookieContainer = new System.Net.CookieContainer();
  70.                     handler.UseCookies = true;
  71.                     client = new HttpClient(handler);
  72.                     client.MaxResponseContentBufferSize = 256000;
  73.                     client.DefaultRequestHeaders.Add(“User-Agent”, “Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)”);
  74.                     client.DefaultRequestHeaders.Add(“Connection”, “Keep-Alive”);
  75.                     client.DefaultRequestHeaders.ExpectContinue = false;
  76.                 }
  77.                 return client;
  78.             }
  79.         }
  80.         private async void button1_Click(object sender, EventArgs e)
  81.         {
  82.             string url = String.Format(“{0}?loginToRp={1}”, textBox1.Text, HttpUtility.UrlEncode(comboBox1.SelectedItem.ToString()));
  83.             // Limit the max buffer size for the response so we don’t get overwhelmed
  84.             HttpResponseMessage result;
  85.             textBox3.Text=“============== Start ===============”;
  86.             var nl = Environment.NewLine;
  87.        
  88.             string text;
  89.             do
  90.             {
  91.                 textBox3.AppendText(String.Format(“{1}********** GET {0}{1}”, url, Environment.NewLine));
  92.                 result = await Client.GetAsync(url);
  93.                 text = await result.Content.ReadAsStringAsync();
  94.                 IEnumerable<string> values;
  95.                 if(result.Headers.TryGetValues(“location”, out values))
  96.                 {
  97.                     foreach(string s in values)
  98.                     {
  99.                         if (s.StartsWith(“/”))
  100.                         {
  101.                             url = url.Substring(0, url.IndexOf(“/adfs/ls”)) + s;
  102.                         }
  103.                         else
  104.                             url = s;
  105.                     }
  106.                 } else
  107.                 {
  108.                     url = “”;
  109.                 }
  110.                 textBox3.AppendText(String.Format(“{0}[Headers]{0}”, Environment.NewLine));
  111.                 foreach(var pair in result.Headers)
  112.                 {
  113.                     string key = pair.Key;
  114.                     foreach(var val in pair.Value)
  115.                         textBox3.AppendText(String.Format(” {0}={1}{2}”, key, val, Environment.NewLine));
  116.                 }
  117.                 textBox3.AppendText(text);
  118.             } while (!String.IsNullOrEmpty(url));
  119.             Regex reg = new Regex(“SAMLResponse\\W+value\\=\\\”([^\\\”]+)\\\””);
  120.             MatchCollection matches = reg.Matches(text);
  121.             string last = null;
  122.             foreach (Match m in matches)
  123.             {
  124.                 last = m.Groups[1].Value;
  125.                 textBox3.AppendText(String.Format(” {1}{1}{1}SAMLResponse={0}{1}”, last, Environment.NewLine));
  126.             }
  127.             if(last != null)
  128.             {
  129.                 byte[] decoded = Convert.FromBase64String(last);
  130.                 string deflated = Encoding.UTF8.GetString(decoded);
  131.                 XmlDocument doc = new XmlDocument();
  132.                 StringBuilder sb = new StringBuilder();
  133.                 doc.LoadXml(deflated);
  134.                 using(StringWriter sw = new StringWriter(sb))
  135.                 {
  136.                     using (XmlTextWriter tw = new XmlTextWriter(sw) { Formatting = Formatting.Indented })
  137.                     {
  138.                         doc.WriteTo(tw);
  139.                     }
  140.                 }
  141.                 textBox3.AppendText(String.Format(” {1}{1}{1}XML Formated:{1}{0}{1}”, sb.ToString(), Environment.NewLine));
  142.                 
  143.             }
  144.         }
  145.         private void DysplayError(Exception ex)
  146.         {
  147.             MessageBox.Show(String.Format(“Error: {0}{1}Stack:{1}{2}”, ex.Message, Environment.NewLine, ex.StackTrace));
  148.         }
  149.         private async void button2_Click(object sender, EventArgs e)
  150.         {
  151.             try
  152.             {
  153.                 var response = await Client.GetAsync(textBox1.Text);
  154.                 response.EnsureSuccessStatusCode();
  155.                 comboBox1.Items.Clear();
  156.                 string text = await response.Content.ReadAsStringAsync();
  157.                 Regex reg = new Regex(“option\\W+value\\=\\\”([^\\\”]+)\\\””);
  158.                 MatchCollection matches = reg.Matches(text);
  159.                 foreach(Match m in matches)
  160.                 {
  161.                     comboBox1.Items.Add(m.Groups[1].Value);
  162.                 }
  163.                 if (matches.Count == 0)
  164.                 {
  165.                     MessageBox.Show(“No Reliant Party found”);
  166.                     button1.Enabled = false;
  167.                 } else
  168.                 {
  169.                     button1.Enabled = true;
  170.                     comboBox1.SelectedIndex = 0;
  171.                 }
  172.             } catch(Exception ex)
  173.             {
  174.                 DysplayError(ex);
  175.                 return;
  176.             }
  177.         }
  178.     }
  179. }

I also organized the code in a class library that can be used in any C# application. You can download the code and test application here.

Class to get SAML Response
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Threading.Tasks;
  5. using System.Net.Http;
  6. using System.Text.RegularExpressions;
  7. using System.Web;
  8. using System.Xml;
  9. using System.IO;
  10. //The code samples are provided AS IS without warranty of any kind.
  11. // Microsoft disclaims all implied warranties including, without limitation,
  12. // any implied warranties of merchantability or of fitness for a particular purpose.
  13. /*
  14. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you.
  15. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts
  16. be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption,
  17. loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts
  18. or documentation, even if Microsoft has been advised of the possibility of such damages.
  19. */
  20. namespace Microsoft.Samples.AdfsSaml
  21. {
  22.     /// <summary>
  23.     /// Class to get a SAML response from ADFS.
  24.     /// it requires that the SAML endpoint with POST binding is configured in ADFS
  25.     /// </summary>
  26.     public class SamlResponse
  27.     {
  28.         /// <summary>
  29.         /// If true, ADFS url will not be validated
  30.         /// </summary>
  31.         public bool EnableRawUrl
  32.         {
  33.             get;
  34.             set;
  35.         }
  36.         private Uri serverAddress = null;
  37.         private HttpClient client = null;
  38.         private HttpClientHandler handler;
  39.         private HttpClient Client
  40.         {
  41.             get
  42.             {
  43.                 if (client == null)
  44.                 {
  45.                     handler = new HttpClientHandler();
  46.                     handler.UseDefaultCredentials = true;
  47.                     handler.AllowAutoRedirect = false;
  48.                     handler.CookieContainer = new System.Net.CookieContainer();
  49.                     handler.UseCookies = true;
  50.                     client = new HttpClient(handler);
  51.                     client.MaxResponseContentBufferSize = 256000;
  52.                     client.DefaultRequestHeaders.Add(“User-Agent”, “Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)”);
  53.                     client.DefaultRequestHeaders.Add(“Connection”, “Keep-Alive”);
  54.                     client.DefaultRequestHeaders.ExpectContinue = false;
  55.                 }
  56.                 return client;
  57.             }
  58.         }
  59.         /// <summary>
  60.         /// Url of ADFS server (e.g. https://adfs.contoso.com)
  61.         /// </summary>
  62.         public Uri ServerAddress
  63.         {
  64.             get
  65.             {
  66.                 return serverAddress;
  67.             }
  68.             set
  69.             {
  70.                 if(EnableRawUrl)
  71.                 {
  72.                     serverAddress = value;
  73.                     return;
  74.                 }
  75.                 string host = value.Host;
  76.                 string scheme = value.Scheme;
  77.                 if(scheme != “https”)
  78.                 {
  79.                     throw new ArgumentException(“ADFS rquires scheme https. Set EnableRawUrl to true to override it”, “ServerAddress”);
  80.                 }
  81.                 serverAddress = new Uri(String.Format(“https://{0}//adfs/ls/IdpInitiatedSignOn.aspx”, host));
  82.             }
  83.         }
  84.         /// <summary>
  85.         /// Initialize the class
  86.         /// </summary>
  87.         public SamlResponse()
  88.         {
  89.         }
  90.         /// <summary>
  91.         /// Initialize the class
  92.         /// </summary>
  93.         /// <param name=”Url”>Urn of reliant party as defined in ADFS. Ise GetReliantPArtyCollection to get the list</param>
  94.         /// <param name=”IsRawUrl”>If true, ADFS url will not be validated</param>
  95.         public SamlResponse(Uri Url, bool IsRawUrl = false)
  96.         {
  97.             EnableRawUrl = IsRawUrl;
  98.             ServerAddress = Url;
  99.         }
  100.         private async Task<string[]> GetReliantPartyCollectionInternal()
  101.         {
  102.             if (serverAddress == null)
  103.             {
  104.                 throw new NullReferenceException(“ServerAddress was not set”);
  105.             }
  106.             var response = await Client.GetAsync(serverAddress);
  107.             response.EnsureSuccessStatusCode();
  108.             string text = await response.Content.ReadAsStringAsync();
  109.             Regex reg = new Regex(“option\\W+value\\=\\\”([^\\\”]+)\\\””);
  110.             MatchCollection matches = reg.Matches(text);
  111.             if(matches.Count == 0)
  112.             {
  113.                 return null;
  114.             }
  115.             string[] rps = new string[matches.Count];
  116.             uint i = 0;
  117.             foreach (Match m in matches)
  118.             {
  119.                 rps[i++]=m.Groups[1].Value;
  120.             }
  121.             return rps;
  122.         }
  123.         /// <summary>
  124.         /// Get the list of Reliant Parties with SAML endpoint with binding POST in ADFS
  125.         /// </summary>
  126.         public string[] GetReliantPartyCollection()
  127.         {
  128.             return GetReliantPartyCollectionInternal().Result;
  129.         }
  130.         /// <summary>
  131.         /// Retrieve the SAML Response from ADFS for ReliantPartyUrn
  132.         /// </summary>
  133.         /// <param name=”ReliantPartyUrn”>Urn of reliant party as defined in ADFS. Ise GetReliantPArtyCollection to get the list</param>
  134.         public string RequestSamlResponse(string ReliantPartyUrn)
  135.         {
  136.             if(serverAddress == null)
  137.             {
  138.                 throw new NullReferenceException(“ServerAddress was not set”);
  139.             }
  140.             if(String.IsNullOrEmpty(ReliantPartyUrn) && !EnableRawUrl)
  141.             {
  142.                 throw new ArgumentException(“Reliant Party Urn cannot be empty if EnableRawUrl is not true”);
  143.             }
  144.             return SamlResponseInternal(ReliantPartyUrn).Result;
  145.             
  146.         }
  147.         private async Task<string> SamlResponseInternal(string ReliantPartyUrn)
  148.         {
  149.             StringBuilder url = new StringBuilder(String.Format(“{0}?loginToRp={1}”, serverAddress, HttpUtility.UrlEncode(ReliantPartyUrn)));
  150.             HttpResponseMessage result;
  151.             
  152.             do
  153.             {
  154.                 result = await Client.GetAsync(url.ToString());
  155.                 string text = await result.Content.ReadAsStringAsync();
  156.                 IEnumerable<string> values;
  157.                 if (result.Headers.TryGetValues(“location”, out values))
  158.                 {
  159.                     foreach (string s in values)
  160.                     {
  161.                         if (s.StartsWith(“/”))
  162.                         {
  163.                             string newUrl = url.ToString().Substring(0, url.ToString().IndexOf(“/adfs/ls”));
  164.                             url.Clear();
  165.                             url.Append(newUrl);
  166.                             url.Append(s);
  167.                         }
  168.                         else
  169.                         {
  170.                             url.Clear();
  171.                             url.Append(s);
  172.                         }
  173.                     }
  174.                 }
  175.                 else
  176.                 {
  177.                     url.Clear();
  178.                 }
  179.                 if (url.Length == 0)
  180.                 {
  181.                     Regex reg = new Regex(“SAMLResponse\\W+value\\=\\\”([^\\\”]+)\\\””);
  182.                     MatchCollection matches = reg.Matches(text);
  183.                     foreach (Match m in matches)
  184.                     {
  185.                         return m.Groups[1].Value;
  186.                     }
  187.                 }
  188.             } while (url.Length > 0);
  189.             throw new InvalidOperationException(“Unable to get a SAMLP response from ADFS”);
  190.         }
  191.         public static string SamlToXmlString(string EncodedResponse)
  192.         {
  193.             byte[] decoded = Convert.FromBase64String(EncodedResponse);
  194.             string deflated = Encoding.UTF8.GetString(decoded);
  195.             XmlDocument doc = new XmlDocument();
  196.             StringBuilder sb = new StringBuilder();
  197.             doc.LoadXml(deflated);
  198.             using (StringWriter sw = new StringWriter(sb))
  199.             {
  200.                 using (XmlTextWriter tw = new XmlTextWriter(sw) { Formatting = Formatting.Indented })
  201.                 {
  202.                     doc.WriteTo(tw);
  203.                 }
  204.             }
  205.             return sb.ToString();
  206.         }
  207.     }
  208. }
30 Comments