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

2

23

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));
    }
}
Throwaway answered 23/8, 2012 at 11:19 Comment(8)
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.Saad
Can you post your working code please.Throwaway
Hey Randall, can you provide the code for the changes you made to "Decode SAMLResponse - Working Code"? That would be much appreciated! ThanksOrganization
I updated the code above per your request.Saad
@RandallBorck Please post your solution as an answer!Ligure
@Throwaway please accept the edit change to the Decode SAMLResponse - Working Code section.Saad
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.Throwaway
@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.Throwaway
L
14

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.

Labroid answered 23/8, 2012 at 11:42 Comment(2)
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?Labroid
I've found a compresser that works with the sample site, and I've updated the answer yet again. Please reread the last part :)Labroid
S
14

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());
    }
  }
}
Saad answered 9/4, 2013 at 18:34 Comment(1)
This is the perfect answer!Carranza

© 2022 - 2024 — McMap. All rights reserved.