Geeks With Blogs
blog.davidbarrett.net ///<summary>Just a lot of random technical stuff, really.</summary>
I recently had a requirement to interop with a web service (written in Java, not that it matters all that much what it was written in) with a quasi-unique set of security requirements.  They were as follows:

  • SOAP 1.1
  • Transport security was an option.  Production endpoint was using SSL; test endpoint was not.  Need the flexibility to turn this on or off.
  • Message Security consisted of two tokens (both WS-Security 1.0)
    • Unsigned username token with plaintext password (forget the argument about plain text using a non-encrypted channel)
    • X.509 token signing the SOAP message body
      • This is a client cert to authenticate the client to the service
      • X.509 token was not embedded within the message itself; it was an external reference to a cert using a thumbprint lookup
I started down the path of using WCF.  None of the out-of-the-box bindings fit the bill for this, so I realized I needed to write some custom stuff.  However, I soon realized I was mired in a bog of custom code to get this to work.  I had started to create a custom binding (with a "two-token" binding element), but I was faced with a myriad of complexities and too little experience to navigate it successfully.  The WCF stack is a highly complex framework, and diving into it to develop completely custom code shouldn't be taken lightly.  I hope to revisit this problem with WCF to implement it the "right" way, considering this is the .NET connected systems technology going forward.  When I do, I'll post the solution here.

Anyway, at this point, I shifted gears and  looked to WSE3.0 to solve this problem.  As with WCF, none of the turnkey assertion policies worked for me, so I had to roll up my sleeves and start working on custom code.  The solution was fairly straightforward, although I had to go through several iterations to turn the various knobs "just so" to get the interop down exactly.

I first started by creating a custom assertion policy, like so:

public class TwoTokenSecurityAssertion : SecurityPolicyAssertion
{
    // Fields
    private TokenProvider<UsernameToken> usernameTokenProvider;
    private TokenProvider<X509SecurityToken> x509TokenProvider;

    // Methods
    public override SoapFilter CreateClientInputFilter(FilterCreationContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        return new ClientInputFilter(this);
    }

    public override SoapFilter CreateClientOutputFilter(FilterCreationContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        return new ClientOutputFilter(this);
    }

    public override SoapFilter CreateServiceInputFilter(FilterCreationContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        return new ServiceInputFilter(this);
    }

    public override SoapFilter CreateServiceOutputFilter(FilterCreationContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        return new ServiceOutputFilter(this);
    }

    public TokenProvider<UsernameToken> UsernameTokenProvider
    {
        get
        {
            return this.usernameTokenProvider;
        }
        set
        {
            this.usernameTokenProvider = value;
        }
    }

    public TokenProvider<X509SecurityToken> X509TokenProvider
    {
        get
        {
            return this.x509TokenProvider;
        }
        set
        {
            this.x509TokenProvider = value;
        }
    }

    // Nested Types

    // Called when a message is inbound to the client
    protected class ClientInputFilter : ReceiveSecurityFilter
    {
        // Methods
        public ClientInputFilter(TwoTokenSecurityAssertion assertion)
            : base(assertion.ServiceActor, true, assertion.ClientActor)
        {
        }

        public override void ValidateMessageSecurity(SoapEnvelope envelope, Security security)
        {
            // no-op

            // We don't expect any security back on the response, so this is a no-op
        }

    }

    // Called when a message is outbound from the client
    protected class ClientOutputFilter : SecureConversationClientSendSecurityFilter
    {
        // Fields
        private UsernameToken upToken;
        private X509SecurityToken x509Token;

        // Methods
        public ClientOutputFilter(TwoTokenSecurityAssertion assertion)
            : base(assertion)
        {
            if (assertion.X509TokenProvider != null)
            {
                this.x509Token = assertion.X509TokenProvider.GetToken();
            }
            if (assertion.UsernameTokenProvider != null)
            {
                this.upToken = assertion.UsernameTokenProvider.GetToken();
            }
        }

        public override void SecureMessage(SoapEnvelope envelope, Security security, MessageProtectionRequirements
                                           message)
        {
            if (envelope == null)
            {
                throw new ArgumentNullException("envelope");
            }
            if (security == null)
            {
                throw new ArgumentNullException("security");
            }

            // Grab the username token and add it as-is to the header
            UsernameToken usernameToken = ClientOutputFilter.ProvideClientToken<UsernameToken>(this.upToken, this.
                                          GetServiceActor(envelope.CurrentSoap));
            security.Tokens.Add(usernameToken);

            // Create the x509 token, but we won't add it to the security headers directly
            X509SecurityToken x509Token = ClientOutputFilter.ProvideServiceToken<X509SecurityToken>(this.x509Token, this.
                                          GetServiceActor(envelope.CurrentSoap));

            // Create a message signature.  We will sign with the x509 token
            MessageSignature signature = new MessageSignature(x509Token);

            // Create a new keyinfo for the x.509 token.  This is so that the signature will have a reference to the
            binary token.
            if (signature.KeyInfo == null)
            {
                signature.KeyInfo = new KeyInfo();
            }

            // create reference so signature can use it.
            SecurityTokenReference tokenRef = new SecurityTokenReference(x509Token, SecurityTokenReference.
                                              SerializationOptions.KeyIdentifier);

            // Add the keyinfo ref to the signature
            signature.KeyInfo.AddClause(tokenRef);

            // we only want to sign the body
            signature.SignatureOptions = message.SignatureOptions;

            // and add the signature to the header
            security.Elements.Add(signature);

        }

        // Written to replace internal static method on CredentialSet
        private static TSecurityToken ProvideClientToken<TSecurityToken>(TSecurityToken token, string actor) where
                                                                         TSecurityToken : SecurityToken
        {

            if (token != null)
            {
                return token;
            }
            TSecurityToken clientToken = default(TSecurityToken);
            if ((actor != null) && (SoapContext.Current != null))
            {
                clientToken = SoapContext.Current.Credentials[actor].GetClientToken<TSecurityToken>();
            }
            if (clientToken == default(TSecurityToken))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unable to determine
                                                    client token to use. Client token type requested was '{0}'. The
                                                    token must be provided either through policy by specifying the token
                                                    in the policy assertion or through code by calling
                                                    WebServicesClientProtocol.SetCredentials or using properties on the
                                                    SoapContext.Credentials.", new object[] { typeof(TSecurityToken).
                                                    ToString() }));
            }
            return clientToken;

        }

        // Written to replace internal static method on CredentialSet
        private static TSecurityToken ProvideServiceToken<TSecurityToken>(TSecurityToken token, string actor) where
                                                                          TSecurityToken : SecurityToken
        {
            if (token != null)
            {
                return token;
            }
            TSecurityToken serviceToken = default(TSecurityToken);
            if ((actor != null) && (SoapContext.Current != null))
            {
                serviceToken = SoapContext.Current.Credentials[actor].GetServiceToken<TSecurityToken>();
            }
            if (serviceToken == default(TSecurityToken))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unable to determine
                                                    service token to use. Service token type requested was '{0}'. The
                                                    token must be provided either through policy by specifying the token
                                                    in the policy assertion or through code by calling
                                                    WebServicesClientProtocol.SetCredentials or using properties on the
                                                    SoapContext.Credentials.", new object[] { typeof(TSecurityToken).
                                                    ToString() }));
            }
            return serviceToken;
        }

    }

    // Not used since we're only dealing with client-side.
    protected class ServiceInputFilter : ReceiveSecurityFilter
    {
        // Fields
        private X509SecurityToken x509Token;

        // Methods
        public ServiceInputFilter(TwoTokenSecurityAssertion assertion)
            : base(assertion.ServiceActor, false)
        {
            if (assertion.X509TokenProvider != null)
            {
                this.x509Token = assertion.X509TokenProvider.GetToken();
            }
        }

        public override void ValidateMessageSecurity(SoapEnvelope envelope, Microsoft.Web.Services3.Security.Security
                                                     security)
        {
            throw new NotImplementedException();
        }
    }

    // Not used since we're only dealing with client-side.
    protected class ServiceOutputFilter : SendSecurityFilter
    {
        // Methods
        public ServiceOutputFilter(TwoTokenSecurityAssertion assertion)
            : base(assertion.ServiceActor, false)
        {
        }

        public override void SecureMessage(SoapEnvelope envelope, Microsoft.Web.Services3.Security.Security security)
        {
            throw new NotImplementedException();
        }
    }
}

Next, I created a web reference to the endpoint and set the WSE3.0 policy in code, as in:

using (MyWebReference.MyInterfaceClient proxy = new MyWebReference.MyInterfaceClient())
{

    // In some ways, "client" vs. "server" is a misnomer here.  What we're actually providing as a client credential is
    a
    // username token, whereas the "server" credential is really a client-side cert to prove to the server that we
    // are who we say we are.
    proxy.SetClientCredential<UsernameToken>(new UsernameToken("sanitized", "sanitized", PasswordOption.SendPlainText));
    proxy.SetServiceCredential<X509SecurityToken>(X509TokenProvider.CreateToken(StoreLocation.LocalMachine, StoreName.
                                                  TrustedPeople, "<sanitized>", X509FindType.FindByThumbprint));

    // Create a custom policy
    Policy myPolicy = new Policy();

    // Create a new policy assertion
    TwoTokenSecurityAssertion myAssertion = new TwoTokenSecurityAssertion();

    // define that we only want to sign the body with the
    myAssertion.Protection.Request.SignatureOptions = SignatureOptions.IncludeSoapBody;

    // Add the assertion to the policy
    myPolicy.Assertions.Add(myAssertion);

    // and finally, set the policy for this client to the custom one we just created
    proxy.SetPolicy(myPolicy);

    proxy.CallMethod();

}

This created a SOAP message as follows, which is exactly what I was looking for:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-
                          instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsa="http://schemas.xmlsoap.
                          org/ws/2004/08/addressing" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-
                          wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-
                          wssecurity-utility-1.0.xsd">
<soap:Header>
  <wsa:Action>
  </wsa:Action>
  <wsa:MessageID>urn:uuid:18667fd7-8a87-4729-a168-ab12379478df</wsa:MessageID>
  <wsa:ReplyTo>
    <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
  </wsa:ReplyTo>
  <wsa:To>https://webserver/destination</wsa:To>
  <wsse:Security soap:mustUnderstand="1">
    <wsu:Timestamp wsu:Id="Timestamp-462c82e9-9d0a-4584-b07f-e7f6e49aefcb">
      <wsu:Created>2008-04-20T20:24:04Z</wsu:Created>
      <wsu:Expires>2008-04-20T20:29:04Z</wsu:Expires>
    </wsu:Timestamp>
    <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
                                  wsu:Id="SecurityToken-ea80bb90-bdc2-4a35-9605-9b6e770b1b4f">
      <wsse:Username>sanitized</wsse:Username>
      <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.
                          0#PasswordText">sanitized</wsse:Password>
      <wsse:Nonce>sanizited</wsse:Nonce>
      <wsu:Created>2008-04-20T20:24:04Z</wsu:Created>
    </wsse:UsernameToken>
    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
      <SignedInfo>
        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" xmlns:ds="http://www.w3.
                                             org/2000/09/xmldsig#" />
        <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
        <Reference URI="#Id-af786227-f58a-441a-b62e-9bb220c6bcbc">
          <Transforms>
            <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
          </Transforms>
          <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
          <DigestValue>sanitized</DigestValue>
        </Reference>
      </SignedInfo>
      <SignatureValue>sanitized</SignatureValue>
      <KeyInfo>
        <wsse:SecurityTokenReference>
          <wsse:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.
                                        1#ThumbprintSHA1" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-
                                        200401-wss-soap-message-security-1.0#Base64Binary">sanitized</wsse:KeyIdentifier>
        </wsse:SecurityTokenReference>
      </KeyInfo>
    </Signature>
  </wsse:Security>
</soap:Header>
<soap:Body wsu:Id="Id-af786227-f58a-441a-b62e-9bb220c6bcbc">
    <TestMethod />
</soap:Body>
</soap:Envelope>

As I mentioned earlier, the "right" way to do this is with WCF, so if I get some time to research doing this with WCF, I'll post my results here. Posted on Monday, April 21, 2008 10:17 AM WCF , SOA | Back to top


Comments on this post: Java Web Service Interop with a .NET Client

# re: Java Web Service Interop with a .NET Client
Requesting Gravatar...
Excellent write up. I am researching how to do this in WCF and will keep you posted as well.
Left by Rick G. Garibay on Apr 27, 2008 2:24 PM

# re: Java Web Service Interop with a .NET Client
Requesting Gravatar...
Hi
i want to write .net client consuming axis webservice using WSE 2.0and .NET 1.1. i need a sample application to build that Client.
Thanks,
Charon
Left by charon on May 11, 2008 5:45 PM

# re: Java Web Service Interop with a .NET Client
Requesting Gravatar...
Tom, not off the top of my head. I was never a big user of WSE.
Left by David Barrett on Sep 17, 2010 8:41 AM

# re: Java Web Service Interop with a .NET Client
Requesting Gravatar...
Sharing a post on building a web service client with WCF, at http://justcompiled.blogspot.com/2010/10/building-web-service-client-with-wcf.html
Left by Shameer on Oct 17, 2010 10:46 AM

Your comment:
 (will show your gravatar)


Copyright © David Barrett | Powered by: GeeksWithBlogs.net