How do I correctly prepare an 'HTTP Redirect Binding' SAML Request using C#

21,901

Solution 1

I've just run the following code with your example SAML:

        var saml = string.Format(sample, Guid.NewGuid());
        var bytes = Encoding.UTF8.GetBytes(saml);

        string middle;
        using (var output = new MemoryStream())
        {
            using (var zip = new DeflaterOutputStream(output))
                zip.Write(bytes, 0, bytes.Length);

            middle = Convert.ToBase64String(output.ToArray());
        }

        string decoded;
        using (var input = new MemoryStream(Convert.FromBase64String(middle)))
        using (var unzip = new InflaterInputStream(input))
        using (var reader = new StreamReader(unzip, Encoding.UTF8))
            decoded = reader.ReadToEnd();

        bool test = decoded == saml;

The test variable is true. This means that the zip/base64/unbase64/unzip roundtrip performs correctly. The error must occur later. Maybe the URLEncoder destroys them? Could you try similar urlencode/decode test? Also, check how long the result is. It may be possible that the resulting URL is truncated due to its length.

(edit: I've added a StreamReader instead of reading to arrays. Earlier my sample used bytes.Length to prepare the buffer and that could damage the test. Now the reading uses only the information from the compressed stream)

edit:

        var saml = string.Format(sample, Guid.NewGuid());
        var bytes = Encoding.UTF8.GetBytes(saml);

        string middle;
        using (var output = new MemoryStream())
        {
            using (var zip = new DeflateStream(output, CompressionMode.Compress))
                zip.Write(bytes, 0, bytes.Length);

            middle = Convert.ToBase64String(output.ToArray());
        }

        // MIDDLE is the thing that should be now UrlEncode'd

        string decoded;
        using (var input = new MemoryStream(Convert.FromBase64String(middle)))
        using (var unzip = new DeflateStream(input, CompressionMode.Decompress))
        using (var reader = new StreamReader(unzip, Encoding.UTF8))
            decoded = reader.ReadToEnd();

        bool test = decoded == saml;

this code produces a middle variable, that once is UrlEncoded, passes through the debugger properly. DeflateStream comes from the standard .Net's System.IO.Compression namespace. I don't have the slightest idea why the SharpZip's Deflate is not accepted by the 'debugger' site. It is undeniable that the compression works, as it manages to decompress the data properly.. it just has to be some difference in the algorithms, but I cannot tell what is the difference between this deflate and that deflate, d'oh.

Solution 2

The question at the top contains a "Decode SAMLResponse - Working Code" section, but that code seemed broken. After trying a few things, I discovered that it was trying to read and write to the same stream at the same time. I reworked it by separating the read and write streams and here is my solution (I am providing the request section for convenience and clarity):

Encode SAML Authentication Request:

public static string EncodeSamlAuthnRequest(this string authnRequest) {
    var bytes = Encoding.UTF8.GetBytes(authnRequest);
    using (var output = new MemoryStream()) {
      using (var zip = new DeflateStream(output, CompressionMode.Compress)) {
        zip.Write(bytes, 0, bytes.Length);
      }
      var base64 = Convert.ToBase64String(output.ToArray());
      return HttpUtility.UrlEncode(base64);
    }
  }

Decode SAML Authentication Response:

public static string DecodeSamlAuthnRequest(this string encodedAuthnRequest) {
  var utf8 = Encoding.UTF8;
  var bytes = Convert.FromBase64String(HttpUtility.UrlDecode(encodedAuthnRequest));
  using (var output = new MemoryStream()) {
    using (var input = new MemoryStream(bytes)) {
      using (var unzip = new DeflateStream(input, CompressionMode.Decompress)) {
        unzip.CopyTo(output, bytes.Length);
        unzip.Close();
      }
      return utf8.GetString(output.ToArray());
    }
  }
}
Share:
21,901
biofractal
Author by

biofractal

Life is the non-random replication of randomly varying replicators

Updated on September 25, 2020

Comments

  • biofractal
    biofractal almost 4 years

    I need to create an SP initiated SAML 2.0 Authentication transaction using HTTP Redirect Binding method. It turns out this is quite easy. Just get the IdP URI and concatenate a single query-string param SAMLRequest. The param is an encoded block of xml that describes the SAML request. So far so good.

    The problem comes when converting the SAML into the query string param. I believe this process of preparation should be:

    1. Build a SAML string
    2. Compress this string
    3. Base64 encode the string
    4. UrlEncode the string.

    The SAML Request

    <samlp:AuthnRequest
        xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
        xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
        ID="{0}"
        Version="2.0"
        AssertionConsumerServiceIndex="0"
        AttributeConsumingServiceIndex="0">
        <saml:Issuer>URN:xx-xx-xx</saml:Issuer>
        <samlp:NameIDPolicy
            AllowCreate="true"
            Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"/>
    </samlp:AuthnRequest>
    

    The Code

    private string GetSAMLHttpRedirectUri(string idpUri)
    {
        var saml = string.Format(SAMLRequest, Guid.NewGuid());
        var bytes = Encoding.UTF8.GetBytes(saml);
        using (var output = new MemoryStream())
        {
            using (var zip = new DeflaterOutputStream(output))
            {
                zip.Write(bytes, 0, bytes.Length);
            }
            var base64 = Convert.ToBase64String(output.ToArray());
            var urlEncode = HttpUtility.UrlEncode(base64);
            return string.Concat(idpUri, "?SAMLRequest=", urlEncode);
        }
    }
    

    I suspect the compression is somehow to blame. I am using the DeflaterOutputStream class from SharpZipLib which is supposed to implement an industry standard deflate-algorithm so perhaps there are some settings here I have wrong?

    The encoded output can be tested using this SAML2.0 Debugger (its a useful online conversion tool). When I decode my output using this tool it comes out as nonsense.

    The question therefore is: Do you know how to convert a SAML string into the correctly deflated and encoded SAMLRequest query-param?

    Thank you

    EDIT 1

    The accepted answer below gives the answer to the problem. Here is final code as corrected by all subsequent comments and answers.

    Encode SAMLRequest - Working Code

    private string GenerateSAMLRequestParam()
    {
        var saml = string.Format(SAMLRequest, Guid.NewGuid());
        var bytes = Encoding.UTF8.GetBytes(saml);
        using (var output = new MemoryStream())
        {
            using (var zip = new DeflateStream(output, CompressionMode.Compress))
            {
                zip.Write(bytes, 0, bytes.Length);
            }
            var base64 = Convert.ToBase64String(output.ToArray());
            return HttpUtility.UrlEncode(base64);
        }
    }
    

    The SAMLRequest variable contains the SAML shown at the top of this question.

    Decode SAMLResponse - Working Code

    private string DecodeSAMLResponse(string response)
    {
        var utf8 = Encoding.UTF8;
        var bytes = utf8.GetBytes(response);
        using (var output = new MemoryStream())
        {
            using (new DeflateStream(output, CompressionMode.Decompress))
            {
                output.Write(bytes, 0, bytes.Length);
            }
            var base64 = utf8.GetString(output.ToArray());
            return utf8.GetString(Convert.FromBase64String(base64));
        }
    }
    
    • Randall Borck
      Randall Borck about 11 years
      I was unable to use your "Decode SAMLResponse - Working Code" the way it is written. I had to separate the input from the output MemoryStream. I also had to UrlDecode and Convert.FromBase64String(...) before doing the DeflateStream. Just thought I'd make a note in case it helps the next person.
    • biofractal
      biofractal about 11 years
      Can you post your working code please.
    • Naner
      Naner about 11 years
      Hey Randall, can you provide the code for the changes you made to "Decode SAMLResponse - Working Code"? That would be much appreciated! Thanks
    • Randall Borck
      Randall Borck about 11 years
      I updated the code above per your request.
    • cheesemacfly
      cheesemacfly about 11 years
      @RandallBorck Please post your solution as an answer!
    • Randall Borck
      Randall Borck about 11 years
      @biofractal please accept the edit change to the Decode SAMLResponse - Working Code section.
    • biofractal
      biofractal about 11 years
      Sorry, I don't have sufficient reputation to review edits to questions. However I note that your edit has been rejected by three other reviewers, the reasons given are: 1. "Answer the question, don’t edit the code. That’s what a question is for." 2. "If you have an answer, please post an answer!" 3. "This edit changes too much in the original post; the original meaning or intent of the post would be lost.". Therefore I suggest you post your code as an answer rather than attempt to edit the question.
    • Randall Borck
      Randall Borck about 11 years
      @Naner Please upvote my answer if you find it helpful. Thanks!
    • Randall Borck
      Randall Borck about 11 years
      @cheesemacfly Please upvote my answer if you find it helpful. Thanks!
    • biofractal
      biofractal about 11 years
      @RandallBorck I have added some curly brackets to the using statement to prevent the issue you noted. This is the version I have in production so I am pretty confident it works. Your version depends on NET Framework 4.5 which I am not allowed to use in this project. Sorry for the typo.
  • quetzalcoatl
    quetzalcoatl almost 12 years
    the debugger site you've provided displays an example of encoded message. I've tried to decompress it with Deflate and GZip - and it was not possible.. Not even the .Net's GZipStream manages to decompress that.. What compression algorithm does it use?
  • quetzalcoatl
    quetzalcoatl almost 12 years
    I've found a compresser that works with the sample site, and I've updated the answer yet again. Please reread the last part :)
  • Srinivasa Rao
    Srinivasa Rao over 3 years
    This is the perfect answer!