Monday, September 17, 2018

Parsing SAML 1.1 (WS-Federation) tokens without the WSFam module

Ocassionally there's a scenario where a SAML token must be parsed without the WSFederationAuthentication module. Note that when the WSFam can be used, parsing is straightforward.

For us, it was one of our old applications that still can't be upgraded to .NET 4.5, because of reasons ;), and we wanted to drop the old WIF runtime (the one that targets older .NET versions). For someone else it can be another scenario, e.g. you have the SAML token as string and just want the IPrincipal out of the token.

The solution is to think of the token as it was the regular XMLDsig signed XML - the assertion node is signed and the signature's reference points back to it:

<?xml version="1.0"?>
<t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
  <t:Lifetime>
    <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2018-09-18</wsu:Created>
    <wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2018-09-18</wsu:Expires>
  </t:Lifetime>
  <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
    <wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
      <wsa:Address>https://foo.bar/</wsa:Address>
    </wsa:EndpointReference>
  </wsp:AppliesTo>
  <t:RequestedSecurityToken>
    <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" MajorVersion="1" MinorVersion="1" AssertionID="_assertionID" 
        Issuer="http://issuer" IssueInstant="2018-09-18">
      <saml:AttributeStatement>
        <saml:Subject>
          <saml:SubjectConfirmation>
            <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
          </saml:SubjectConfirmation>
        </saml:Subject>
        <saml:Attribute AttributeName="windowsaccountname" AttributeNamespace="http://schemas.microsoft.com/ws/2008/06/identity/claims">
          <saml:AttributeValue>username</saml:AttributeValue>
        </saml:Attribute>
      </saml:AttributeStatement>
      <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:SignedInfo>
          <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
          <ds:Reference URI="#_assertionID">
            <ds:Transforms>
              <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
              <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
            <ds:DigestValue>digest</ds:DigestValue>
          </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>signature</ds:SignatureValue>
        <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
          <X509Data>
            <X509Certificate>certificate</X509Certificate>
          </X509Data>
        </KeyInfo>
      </ds:Signature>
    </saml:Assertion>
  </t:RequestedSecurityToken>
  <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
</t:RequestSecurityTokenResponse>
What you should do is to
  1. validate the signature
  2. accept or reject the signature's certificate
  3. parse the token to retrieve claims required to create the IPrincipal
The code is rather simple, what's interesting however is that the SignedXml class has to be inherited to have the signature validator that follows the AssertionID attribute (the default convention is that the signed node's id attribute is called just ID and the default validator just won't find the node that has the id attribute called differently):
    public class SamlSignedXml : SignedXml
    {
        public SamlSignedXml(XmlElement e) : base(e) { }

        public override XmlElement GetIdElement(XmlDocument document, string idValue)
        {
            XmlNamespaceManager mgr = new XmlNamespaceManager(document.NameTable);
            mgr.AddNamespace("trust", "http://docs.oasis-open.org/ws-sx/ws-trust/200512");
            mgr.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
            mgr.AddNamespace("saml", "urn:oasis:names:tc:SAML:1.0:assertion");

            XmlElement assertionNode = 
                   (XmlElement)document.SelectSingleNode("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/"+
                                                         "trust:RequestedSecurityToken/saml:Assertion", mgr);

            if (assertionNode.Attributes["AssertionID"] != null &&
                string.Equals(assertionNode.Attributes["AssertionID"].Value, idValue, StringComparison.InvariantCultureIgnoreCase)
                )
                return assertionNode;

            return null;
        }
    }
Note that the XPath assumes the token has the RequestSecurityTokenResponseCollection in the root, make sure your tokens follow this convention (in case of a single token, the collection node can be missing and the token's root could be just RequestSecurityTokenResponse, update the code accordingly).

The validation code is then

// token is the string representation of the SAML1 token
// expectedCertThumb is the expected certificate's thumbprint
protected bool ValidateToken( string token, string expectedCertThumb, out string userName )
{
 userName = string.Empty;

 if (string.IsNullOrEmpty(token)) return false;

 var xd = new XmlDocument();
 xd.PreserveWhitespace = true;
 xd.LoadXml(token);

 XmlNamespaceManager mgr = new XmlNamespaceManager(xd.NameTable);
 mgr.AddNamespace("trust", "http://docs.oasis-open.org/ws-sx/ws-trust/200512");
 mgr.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
 mgr.AddNamespace("saml", "urn:oasis:names:tc:SAML:1.0:assertion");

 // assertion
 XmlElement assertionNode = (XmlElement)xd.SelectSingleNode("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/trust:RequestedSecurityToken/saml:Assertion", mgr);

 // signature
 XmlElement signatureNode = (XmlElement)xd.GetElementsByTagName("Signature")[0];

 var signedXml = new SamlSignedXml( assertionNode );
 signedXml.LoadXml(signatureNode);

 X509Certificate2 certificate = null;
 foreach (KeyInfoClause clause in signedXml.KeyInfo)
 {
  if (clause is KeyInfoX509Data)
  {
   if (((KeyInfoX509Data)clause).Certificates.Count > 0)
   {
    certificate =
    (X509Certificate2)((KeyInfoX509Data)clause).Certificates[0];
   }
  }
 }

 // cert node missing
 if (certificate == null) return false;

 // check the signature and return the result.
 var signatureValidationResult = signedXml.CheckSignature(certificate, true);

 if (signatureValidationResult == false) return false;

 // validate cert thumb
 if ( !string.IsNullOrEmpty( expectedCertThumb ) )
 {
  if ( !string.Equals( expectedCertThumb, certificate.Thumbprint ) )
   return false;
 }

 // retrieve username

 // expires = 
 var expNode = xd.SelectSingleNode("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/trust:Lifetime/wsu:Expires", mgr );

 DateTime expireDate;

 if (!DateTime.TryParse(expNode.InnerText, out expireDate)) return false; // wrong date

 if (DateTime.UtcNow > expireDate) return false; // token too old

 // claims
 var claimNodes =                 
   xd.SelectNodes("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/trust:RequestedSecurityToken/"+
                  "saml:Assertion/saml:AttributeStatement/saml:Attribute", mgr );
 foreach ( XmlNode claimNode in claimNodes )
 {
  if ( claimNode.Attributes["AttributeName"] != null && 
              claimNode.Attributes["AttributeNamespace"] != null &&
       string.Equals( claimNode.Attributes["AttributeName"].Value, "name", StringComparison.InvariantCultureIgnoreCase ) &&   
                     string.Equals( claimNode.Attributes["AttributeNamespace"].Value, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims", StringComparison.InvariantCultureIgnoreCase ) &&
         claimNode.ChildNodes.Count == 1 
      )
  {
   userName = claimNode.ChildNodes[0].InnerText;
   return true;
  }
 }

 return false;
}
A couple of comments here.

First, the XPath could possibly be shortened to reflect the possibility of a missing collection node.

Then, the code assumes there's the name claim that contains the username but it could be the windowsaccountname or maybe yet another claim type.

Friday, September 14, 2018

WCF and default serialization of requests and responses

A short story of something new we've learned about how exactly WCF serializes the data that is sent over the wire.

Introduction

Before WCF, the default way to serialize objects to XML was to use the XmlSerializer. It works and of course has its shortcomings when it comes to serialization of complex types and collections.

When WCF was introduced, a couple of new serializers were brought into the Base Class Library, including the DataContractSerializer and NetDataContractSerializer. New serializers mean new features, comparision charts are available (e.g. this one by Sebasian Krysmanski).

If you, like we did, live in a simple world where WCF just uses the new set of serializers, read on.

Usually, where both the service and the client are .NET apps, web services can be designed by writing down C# interfaces and data models first. I'd call this common approach the code first approach - you share a code between the service and the client:

// common, shared between the service and the client
[DataContract]
public class DataModel 
{
   [DataMember]
   public string Whatever { get; set; }
}

public interface IServiceContract
{
    void DoWork( DataModel model );
}
Then, the server just implements the interface and exposes the service using a service host (IIS/self-host):
[ServiceBehavior(...)]
public class ServiceImpl : IServiceContract
{
   ...
}
and the client uses the ChannelFactory or the ClientBase to easily have the proxy based on the same interface.

A case of a unit test

Working on a complex integration project involving interoperable calls between a .NET client and a Java WebService, we were faced with an approach we haven't followed often before. Instead of the usual code first approach, we were given a couple of *.WSDL/*.XSD files, which makes a valid model first approach. Given these, you use an automated tool like the xsd.exe or the newer svcutil.exe to automatically create code from models:

svcutil.exe /syncOnly /n:*,Test *.wsdl *.xsd

This approach was used, the code has been generated and someone tried to write a unit test to make sure the request body is correctly serialized so that it meets the XML structure expected at the Java's side. The unit test code first used the DataContractSerializer as we believed this is what WCF uses under the hood. The test code was basically something like:

DoWorkRequest request = new DoWorkRequest();
request.model = 

var serializer = new DataContractSerializer();
var ms = new MemoryStream();

serializer.WriteObject( ms, request );

var requestXML = Encoding.UTF8.GetString( ms.ToArray() );

Assert.....

As it turned out, the serializer's output was something like

<DoWorkRequest ....
while the server's expectation was
<DoWork ....
(note the Request suffix missing from the root's name)

The test was obviously failing. We started an investigation.

What is really going on under the hood

After a couple of different trials and errors involving other serializers and their settings, we've found something that we never manually put into the code in the code-first approach. It was the MessageContractAttribute put over the request class by the generator:

[MessageContractAttribute(WrapperName="DoWork"....]
public class DoWorkRequest {
Things started to get interesting, it looks like there's yet another serializer, not mentioned that much, that obviously respects this attribute. Googling around reveals that there is indeed yet another layer used by WCF on top of different serializers to have even more control on how your data is serialized when a web service is called. This directly leads to the TypedMessageConverter class and code snippets people already posted (e.g this one by Stanislav Dvoychenko).

A solution, finally

The solution was to rewrite the unit test to actually use the TypedMessageConverter:

var request                       = new DoWorkRequest(...);

var converter                     = TypedMessageConverter.Create( request.GetType(), "*", string.Empty, new XmlSerializerFormatAttribute());
var message                       = converter.ToMessage(request, MessageVersion.Soap11WSAddressing10);            

var writerSettings                = new XmlWriterSettings();
writerSettings.OmitXmlDeclaration = true;

var stream                        = new MemoryStream();
var writer                        = XmlWriter.Create(stream, writerSettings);

message.WriteMessage(writer);
writer.Flush();

var requsetXML = Encoding.UTF8.GetString(stream.ToArray());
which gives the exact SOAP message that can be peeked using an HTTP debugger (you can possibly unpack the soap envelope it's wrapped into in your unit test code).