Interop – Part III

This is part three of a multi-part series that describes a security interoperability project between a Secure Token Service (STS) built from Microsoft’s Windows Identify Foundation (WIF) and two open-source (java and ruby) web applications. The primary focus of the project was to secure the java/ruby web services using the STS.

Part one of the series provided an overview of the project, a general discussion of claims based authentication, and a peak at early progress. Part two of the series describes early stages of development and includes a description of some tools and libraries as well as several roadblocks. This installment will disclose the latter stages of the project and include code from the final solution.

Progress from this point forward followed three fairly distinct stages:

  1. Build a test driver that can send the STS a message
  2. Refine the message such that the STS can understand and process the message
  3. Decrypt and decode the response returned by the STS

Stage I – “Help me Obi-Wan Kenobi. You’re my only hope.”

As noted at the end of part two, progress accelerated after we discovered Apache CXF. Using CXF, we were able to build client stubs that, presumably, would allow communication with the STS. The client build process generated several packages and many classes. After a bit of spelunking through the resultant directories, we found the most likely candidates for a client driver in the securitytokenservice package. This package was generated here:

../com/microsoft/schemas/ws/_2008/_06/identity/securitytokenservice/

The package contained two classes:

  1. IWSTrust13Sync.java
  2. SecurityTokenService.java

The SecurityTokenService class defined methods for all the services described in the WSDL and was the most logical entry point. Building the client driver required manual inspection of the CXF generated classes, many Google searches, and a lots of brute force build/compile/debug. Within a day, I was proudly compiling code that could fire off a request to the STS and then receive a response. Unfortunately, the response was an error.

That first attempt looked roughly like this:

// A few of the more interesting imports
import javax.xml.namespace.QName;
import org.oasis_open.docs.ws_sx.ws_trust._200512.RequestSecurityTokenType;
import org.oasis_open.docs.ws_sx.ws_trust._200512.RequestSecurityTokenResponseCollectionType;
import com.microsoft.schemas.ws._2008._06.identity.securitytokenservice.IWSTrust13Sync;
import com.microsoft.schemas.ws._2008._06.identity.securitytokenservice.SecurityTokenService;

// Buid endpoint using a reference to the wsdl
URL endPoint = new URL("https://mydomain/services/issue.svc?wsdl");
SecurityTokenService srv = new SecurityTokenService(endPoint);

// Build a request for a security token
RequestSecurityTokenType token = new RequestSecurityTokenType();

HashMapa = new HashMap();

a.put(new QName("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"), "jdoe");
a.put(new QName("http://schemas.autobase.net/identity/claims/passwordhash"), Base64.encode("test"));
token.getOtherAttributes().putAll(a);

// There were four endpoints defined in SecurityTokenService. Try one at random
IWSTrust13Sync sync = srv.getUserNameWSTrustBindingIWSTrust13Sync();

RequestSecurityTokenResponseCollectionType result = sync.trust13Issue(token);

This code would throw an exception complaining about missing elements.

Stage II – “Stay on Target”

After more trial and error, we deduced that the XML document the code generated was not syntactically correct. Generally, the error messages the STS returned would hint at the cause, particularly when the issue was a missing XML element. Other times we leveraged Google searches, poured through the CXF documentation and/or made random changes until we received an error that made sense.

The final XML document the STS understood looked roughly like this:

<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
  <soap:Header>...</soap:Header>
  <soap:Body>
    <RequestSecurityToken xmlns="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
      <TokenType>http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0</TokenType>
      <RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</RequestType>
      <AppliesTo:AppliesTo xmlns="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:AppliesTo="http://schemas.xmlsoap.org/ws/2004/09/policy">
        <EndpointReference:EndpointReference xmlns="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:EndpointReference="http://schemas.xmlsoap.org/ws/2004/08/addressing">
          <Address>https://mydomain</Address>
        </EndpointReference:EndpointReference>
      </AppliesTo:AppliesTo>
      <Claims Dialect="http://schemas.xmlsoap.org/ws/2005/05/identity">...</Claims>
    </RequestSecurityToken>
  </soap:Body>
</soap:Envelope>

And the code to generate that XML …

// Use the empty constructor – no need to specify wsdl
SecurityTokenService src = new SecurityTokenService();

// Pull the class used to negotiate WS Trust directly from the
// SecurityTokenService
IWSTrust13Sync trust = src.getUserNameWSTrustBindingIWSTrust13Sync();

// *********************************************************************
// To enable WS-Security within CXF for a server or a client, we need to
// set up the WSS4J interceptors.
//
// ... Many Bothans died to bring us this information …
// *********************************************************************

// Obtain a reference to the CXF endpoint using the ClientProxy helper:
Client client = ClientProxy.getClient(trust);

// Set up logging if desired
client.getOutInterceptors().add(new LoggingOutInterceptor());
client.getInInterceptors().add(new LoggingInInterceptor());

// Specify the user we want to authenticate
client.getRequestContext().put("ws-security.username", "SomeUsername");
client.getRequestContext().put("ws-security.password", "UserNamesPassword");

// *********************************************************************
// This was the hard part – add missing XML required by the STS
// *********************************************************************
RequestSecurityTokenType token = new RequestSecurityTokenType();

Element tokenType = createElement( null, "http://docs.oasis-open.org/ws-sx/ws-trust/200512", "TokenType");
tokenType.setTextContent("http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0");
token.getAny().add(tokenType);

Element requestType = createElement(null, "http://docs.oasis-open.org/ws-sx/ws-trust/200512", "RequestType");
requestType.setTextContent("http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue");
token.getAny().add(requestType);

//We never determined why this was needed. According to the documentation found, appliesTo should be optional
Document appliesTodoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();

Element appliesTo = createElement(appliesTodoc, "http://schemas.xmlsoap.org/ws/2004/09/policy", "AppliesTo");
Element endPoint  = createElement(appliesTodoc, "http://schemas.xmlsoap.org/ws/2004/08/addressing", "EndpointReference");
Element address   = createElement(appliesTodoc, "http://schemas.xmlsoap.org/ws/2004/08/addressing", "Address");

address.setTextContent("https://mydomain");
endPoint.appendChild( address );
appliesTo.appendChild( endPoint );
token.getAny().add(appliesTo);

//Now specify what claims we want back.
Document claimsDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();

Element claims = createElement(claimsDoc, "http://docs.oasis-open.org/ws-sx/ws-trust/200512", "Claims");
claims.setAttribute( "Dialect", "http://schemas.xmlsoap.org/ws/2005/05/identity" );

Element claimTypeEmail = createElement(claimsDoc, "http://schemas.xmlsoap.org/ws/2005/05/identity", "ClaimType");
claimTypeEmail.setAttribute( "Uri", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" );

Element claimTypePasswordHash= createElement(claimsDoc, "http://schemas.xmlsoap.org/ws/2005/05/identity", "ClaimType");
claimTypePasswordHash.setAttribute( "Uri", "http://schemas.mydomain/identity/claims/passwordhash" );

claims.appendChild( claimTypeEmail );
claims.appendChild( claimTypePasswordHash );

// Add claims to token request
token.getAny().add( claims );

// Make the request
RequestSecurityTokenResponseCollectionType result = trust.trust13Issue(token);

Stage III – “You’re all clear, kid! Now let’s blow this thing and go home!”

The STS response to a syntactically valid authentication request is a SAML document. SAML is is an XML-based open standard data format for exchanging authentication and authorization data between parties. Oh yeah, and it happens to be encrypted, which means we really had no idea if our driver was working. Other than a complicated SOAP envelope, all we could see was gobbledygook.

Fortunately, deciphering the SAML response was the most straightforward part of the entire project. Once we received the certificate used to encrypt and sign the token, it was just a matter of following examples found on-line. The hardest part was realizing that a certificate created in .NET required conversion into a format java could understand (a java keystore). We used a flavor of program called keytool available from a project called Jetty to convert the certificate.

With a java keystore in hand, the code to decrypt the SAML message looks like this:

// Most of the SAML processing classes are located here
import org.opensaml.*;

RequestSecurityTokenResponseCollectionType result = trust.trust13Issue(token);

List<Object> tempList = result.getRequestSecurityTokenResponse().get( 0 ).getAny();

// Find the encrypted element
Iterator<Object> tli = tempList.iterator();

while(tli.hasNext())
{
   Element e = ((Element)tli.next());

   if(e.getLocalName().equals( "RequestedSecurityToken" ))
   {
      Element encryptedAssertion = (Element)(e.getFirstChild());

      // Initialize SAML libs
      DefaultBootstrap.bootstrap();

      Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(EncryptedAssertion.DEFAULT_ELEMENT_NAME);

      XMLObject xo = unmarshaller.unmarshall(encryptedAssertion);
      EncryptedAssertion encryptedAssertion2 = (EncryptedAssertion)xo;

      // The cert we were given was created in .NET. Need to convert that to a java keystore
      // We used a Jetty utility to do that
      File keyStoreFile = new File("/home/rgarabedian/authentication-ws/jetty/jetty-6.1.1/MyCert.jks");

      KeyStore keyStore = KeyStore.getInstance("JKS");

      //load up a KeyStore
      keyStore.load(new FileInputStream(keyStoreFile), "keyStorepwd".toCharArray());

      // Get a key. The key name was found using the keytool
      // keytool -list -keystore /home/rgarabedian/authentication-ws/jetty/jetty-6.1.1/MyCert.jks
      RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey("ststestcert235e879a-fe39-48c5-99b6-effa516f3692", "keyStorepwd".toCharArray());

      //create the credential
      BasicX509Credential decryptionCredential = new BasicX509Credential();
      decryptionCredential.setPrivateKey(privateKey);

      StaticKeyInfoCredentialResolver skicr = new StaticKeyInfoCredentialResolver(decryptionCredential);

      // Support EncryptedKey/KeyInfo containing decryption key hints via
      // KeyValue/RSAKeyValue and X509Data/X509Certificate
      List<KeyInfoProvider> kiProviders = new ArrayList<KeyInfoProvider>();
      kiProviders.add( new RSAKeyValueProvider() );
      kiProviders.add( new InlineX509DataProvider() );
      kiProviders.add( new DSAKeyValueProvider() );

      // Supports resolution of EncryptedKeys by 3 common placement mechanisms
      ChainingEncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver();
      encryptedKeyResolver.getResolverChain().add( new InlineEncryptedKeyResolver() );
      encryptedKeyResolver.getResolverChain().add( new EncryptedElementTypeEncryptedKeyResolver() );
      encryptedKeyResolver.getResolverChain().add( new SimpleRetrievalMethodEncryptedKeyResolver() );

      Decrypter samlDecrypter = new Decrypter(null, skicr, encryptedKeyResolver);

      //Moment of truth – decrypt!
      try
      {
         Assertion assertion = samlDecrypter.decrypt(encryptedAssertion2);

         DOMBuilder builder2 = new DOMBuilder();
         org.jdom.Element document2 = builder2.build(assertion.getDOM());

         // Finally! The decrypted response output to debug logs!
         // The claims are at the bottom of the document as attributes, e.g
         // <Attribute Name="http://schemas.autobase.net/identity/claims/passwordhash">
         //   <AttributeValue>
         //     VEx8kTkKXplTvHmkiaAxNnYBchVIriG9z0n3kbHB+v0x9qdDsx+eTaAMyT69YSJ8
         //   </AttributeValue>
         // </Attribute>
         logger.debug( "DOM: " + outputter.outputString(document2));

      }
      catch (DecryptionException ee)
      {
        ee.printStackTrace();
      }
   }
}

And there you have it – these concepts were used as the basis for a system that allowed a windows application to securely access our java web services.

“Look, good against remotes is one thing, good against the living, that’s something else.”

I must confess that at the end of this project we were feeling fairly good about ourselves and the work we had done… that is, until we started the same process with a Ruby application.  That experience will be the subject of the fourth and final part of this series.

4 Responses to “Interop – Part III”

  1. Mikle bey

    Hi,
    I have go through all three article is very nice and help full i am also going through a wcf webservice calling in java can your tell me what exactly you did in createElement function ? it will really help full if you provide the code .
    Thanks in Advance.

    Reply
    • Rich Garabedian, Dev Lead, @utoRevenue/Dominion Dealer Solutions

      It is just a helper function:

      private static Element createElement(Document doc, String ns, String tagName)
      {
      Element element = null;
      try
      {
      if( doc == null)
      {
      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

      DocumentBuilder db = dbf.newDocumentBuilder();
      doc = db.newDocument();
      }

      if(ns != null)
      {
      element = doc.createElementNS(ns, tagName);
      }
      else
      {
      element = doc.createElement( tagName );
      }
      }
      catch(Exception e)
      {
      e.printStackTrace();
      }
      return element;
      }

      Reply
      • Mikle bey

        Hi Thanks for replay can you please provide me some more help.
        I have created xml generation code as you suggest in this blog now i am getting a
        Error when i am issues security token. The error is below javax.xml.ws.soap.SOAPFaultException: None of the policy alternatives can be satisfied.

        What could be the wrong ?

        Reply

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>