Monday, January 14, 2008

saml, inadequacies in communication between tech groups, and ineffective trouble shooting.

Recently one of my developers was involved in an integration with a large service provider where a new[ish] specification/technology was used for the security layer. In this integration (SOAP w/ SAML) the service provider (while having chosen to use the technology) had a fundamental lack of understanding as to how the technology worked (as did I at the time). This was largely to do with the face that the application server chosen was wrapping this functionality and hiding its implementation which did not lend well towards debugging the integration. After five days of my developer chasing his tail trying to figure out a solution due to a limitation (or perceived limitation) in .NET based upon a series of supposed "facts" I was introduced into the mix where I initiated a restatement of FACTS (yes, as in actually facts not assumptions) in which case a solution was quickly identified and it became glaringly apparent that not everyone understood how the technology was used on their end or how the specification enforced their security model (going back to the application server) -- and this is fine. A lot of people have done this and I'm guilty of it as well. This does go back to proper trouble shooting. When faced with a problem always be able to verbalize your known facts in relation to your assumptions and your plain old I-don't-know's. This will give you the ability to identify the correlations between each set and apply an order of precedence to being trouble shooting. Will definitely speed up the process and prevent people from shooting in the dark.

So, if you're ever needing to do an integration of SAML into a WSDL generated SOAP proxy call in .NET then listen up. You can do this integration by deriving from the SoapExtension class and then defining an attribute to be placed on the SOAP proxy call ......


public class MySoapExtension : SoapExtension
{
public bool outgoing = true;
public bool incoming = false;
private Stream outputStream;
public Stream oldStream;
public Stream newStream;

public override Stream ChainStream(Stream stream)
{
this.outputStream = stream;
oldStream = stream;
newStream = new MemoryStream();
return newStream;
}


public string getXMLFromCache()
{
newStream.Position = 0; // start at the beginning!

string soapString = ExtractFromStream(newStream);

return soapString;
}

private String ExtractFromStream(Stream target)
{
if (target != null)
return (new StreamReader(target)).ReadToEnd();
return "";
}

public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
return null;
}
public override object GetInitializer(Type serviceType)
{
return null;
}
public override void Initialize(object initializer)
{
return;
}

public override void ProcessMessage(SoapMessage message)
{
// if we want to add a header we can hijack the output stream
// and load the xml into a document and manipulate it as is
// here we want the AfterSerialize stage

switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
{
// assumed saml operations class
SAMLAssertion assertion = new SAMLAssertion();

// create assertion
assertion.CreateAssertion();

// generate signature
assertion.GenerateAssertionSignature();

Soap11 soap = new Soap11();

String soapBodyString = getXMLFromCache();

XmlDocument doc = new XmlDocument();

doc.LoadXml(soapBodyString);

XmlElement soapHeaderElement = soap.CreateHeaderElement(doc);

XmlElement wsseSecurityElement = doc.CreateElement("wsse", "Security", "http://schemas.xmlsoap.org/ws/2003/06/secext");

wsseSecurityElement.AppendChild(doc.ImportNode(assertion.Xml,true));

soapHeaderElement.AppendChild(wsseSecurityElement);

doc.DocumentElement.PrependChild(soapHeaderElement);

Stream appOutputStream = new MemoryStream();

StreamWriter soapMessageWriter = new StreamWriter(appOutputStream);

soapMessageWriter.Write(doc.InnerXml);

soapMessageWriter.Flush();

appOutputStream.Flush();

appOutputStream.Position = 0;

StreamReader reader = new StreamReader(appOutputStream);

StreamWriter writer = new StreamWriter(this.outputStream);

writer.Write(reader.ReadToEnd());

writer.Flush();

appOutputStream.Close();

this.outgoing = false;
this.incoming = true;

break;
}
case SoapMessageStage.BeforeDeserialize:
break;
case SoapMessageStage.AfterDeserialize:
break;
default:
throw new Exception("invalid stage!");
}
}
}


[AttributeUsage(AttributeTargets.Method)]
public class MySoapExtensionAttribute :
SoapExtensionAttribute
{
private int priority;
public override Type ExtensionType
{
get
{
// return type of extension to load
return typeof(MySoapExtension);
}
}

public override int Priority
{
get { return priority; }
set { priority = value; }
}
}




// And finally the wsdl generated proxy call
[System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://www.somecompany.com/SomeAPI/SomeMethod",
Use=System.Web.Services.Description.SoapBindingUse.Literal,
ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Bare)]
[return: System.Xml.Serialization.XmlElementAttribute("SomeMethod_Response",
Namespace="http://www.somecomapny.com/SomeAPI")]
[MySoapExtensionAttribute]
public SomeMethod_Response SomeMethod([System.Xml.Serialization.XmlElementAttribute(Namespace="http://www.somecompany.com/SomeAPI")] SomeMethod_Request SomeMethod_Request) {
object[] results = this.Invoke("SomeMethod", new object[] {
SomeMethod_Request});
return ((SomeMethod_Response)(results[0]));
}



So, if anyone finds themselves faced with a similar issue in .NET realize that you can hijack the output stream, process the xml as you see fit (and even validate yet again against another xsd) and retransmit the request over the wire... Just remember, what comes back has to be able to be proxied into the response object so Soap:Header information may be lost if not exposed appropriately on the server side to be consumed by the client.

Cheers.

dan

No comments: