(Attempting to) migrate from WSE 3.0 to WCF for client code
Asked Answered
B

1

10

I have been all over the net for this. I've just been having a devil of a time doing it, and the vendor whose web service I'm trying to consume refuses to officially support WCF as a method of consumption.

I'm no web services expert, so I'll do my best to document and explain with this initial post, but by all means, request more information if you need it, and hopefully I'll be able to supply whatever is necessary.

The service

At my company, we use a vendor application that exposes a service. The application is written in java, and it looks like the wsdl was created with Apache Axis 1.2.

The code

My legacy code uses WSE 3.0. In particular, it uses the proxy classes that have "WSE" auto-tacked at the end. This allows me to use a much simpler authentication scheme (the only way I could get it to work). I don't need to use certificates. I use a derivative of SecurityPolicyAssertion, and wrap it in a Policy object that gets passed to the SetPolicy method of the client class. Here's all I need to do to create a working instance of the client:

MyWebServiceWse api = new MyWebServiceWse();
api.Url = myUrl;
api.SetPolicy(new Policy(new MyDerivedSecurityAssertion(user, pass)));

My default, out-of-the-box code for WCF (generated with a service reference) does not accept credentials, so I know there's a problem right off the bat. I've read various things online about using different security or binding settings in my app.config, but nothing has ever completely worked. My most common error after copious tinkering is WSDoAllReceiver: Request does not contain required Security header.

Here's the app.config. Perhaps we could start by telling me what ought to change here to facilitate passing the credentials--again, I've seen varying opinions online.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="MySoapBinding" closeTimeout="00:01:00"
                    openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
                    allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
                    maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
                    messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
                    useDefaultWebProxy="true">
                    <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                        maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                    <security mode="None">
                        <transport clientCredentialType="None" proxyCredentialType="None"
                            realm="" />
                        <message clientCredentialType="UserName" algorithmSuite="Default" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://xyz:12345/services/MyService"
                binding="basicHttpBinding" bindingConfiguration="MySoapBinding"
                contract="MyNS.MyService" name="MyService" />
        </client>
    </system.serviceModel>
</configuration>

I have changed some of the attributes to obscure the specific service we are using (company policy and all that).

And here is the sample C# code so far (testing in a console app):

MyClient client = new MyClient();
client.listMethod();

UPDATE

Read this SO post: wcf security . . ..

I have updated my app.config accordingly, and am now passing username and pwd in code. I am still receiving the same error:

WSDoAllReceiver: Request does not contain required Security header

20120517 UPDATE

A successful request (from WSE3):

  <soap:Header>
    <wsa:Action>
    </wsa:Action>
    <wsa:MessageID>urn:uuid:cb739422-c077-4eec-8cb2-686837b76878</wsa:MessageID>
    <wsa:ReplyTo>
      <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
    </wsa:ReplyTo>
    <wsa:To>http://removed-for-security</wsa:To>
    <wsse:Security soap:mustUnderstand="1">
      <wsu:Timestamp wsu:Id="Timestamp-e13feaf9-33d9-47bf-ab5b-60b4611eb81a">
        <wsu:Created>2012-05-17T11:25:41Z</wsu:Created>
        <wsu:Expires>2012-05-17T11:30:41Z</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-00c26e1a-3b3b-400f-a99a-3aa54cf8c8ff">
        <wsse:Username>change-to-protect-the-innocent</wsse:Username>
        <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">nice-try</wsse:Password>
        <wsse:Nonce>KJMvUuWF2eO2uIJCuxJC4A==</wsse:Nonce>
        <wsu:Created>2012-05-17T11:25:41Z</wsu:Created>
      </wsse:UsernameToken>
    </wsse:Security>
  </soap:Header>
  <soap:Body>
    <listChannels xmlns="http://removed-for-security">
      <rowfrom>0</rowfrom>
      <rowto>10</rowto>
    </listChannels>
  </soap:Body>
</soap:Envelope>

Working on getting the WCF trace--will add shortly.

20120517 UPDATE 2

And here's the envelope from WCF:

  <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    <s:Header>
      <Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none"></Action>
    </s:Header>
    <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
      <listChannels xmlns="http://removed-for-security">
        <rowfrom>1</rowfrom>
        <rowto>2147483647</rowto>
      </listChannels>
    </s:Body>
  </s:Envelope>

20120518 UPDATE I have tried implementing the solution in the post that Mike Miller links to in the comments. Now I receive the following error (no message ends up getting sent because something's barfing on the scheme):

The provided URI scheme 'http' is invalid; expected 'https'.

And in case anyone wants to ask, yes, I need to send over http, and yes, I'm aware that credentials are sent as unencrypted strings :-)

Bifoliolate answered 14/5, 2012 at 19:8 Comment(11)
First thing I would compare is the soap requests generated from both the WSE & WCF clients. Since the WCF client is configured for security mode="None" its likely no soap security header is being created but the captured requests will show that. I believe the basicHttpBinding cannot be configured to support message level security. You'll likely need the wsHttpBinding for that configuration. Lastly, look at what Microsoft has done in the WCF Express Interop project, its wizards may apply to WCF clients too.Glaciology
Thanks, Sixto--what's the best way to capture the request that WCF is sending?Bifoliolate
WCF has a built-in message tracing feature but you could also use Fiddler for capturing traffic from both the WSE and the WCF clients. To expand on my previous comment, you should approach creating your new WCF client from the point of view of interfacing with an Apache Axis service instead of porting from a legacy WSE client. In spite of the "interop" promise of the WS-* standards, it's a real pain getting WCF to work with any non-WCF based service or client.Glaciology
Can you post the wsdl and the actual SOAP requests (failing and working)? You can use SoapUI to create a working request and dump your failing requests.Tortuga
Unfortunately, I cannot post the WSDL--it would violate company policy. I will, however, get you the SOAP request (with identifying elements removed, of course :-)). Think you'll be able to help?Bifoliolate
Is this the same problem? #3103193Monica
Since you have to censor information and that is troublesome to pin point the problem I suggest a different approach: Can you try to create a simple WSE service and a simple WCF client? See how that work - if it not work, paste it here. I think your problem will be solved by you before you get to that but even if not.. at least we will have somthing to tinker with.Bounded
I think the last update points to this direction. No message level client credentials over http with wcf. #9191604Viscardi
just a ping that I've edited my answer and it now contains the actual implementationGalvin
Thanks--will try to implement once I'm back at work tomorrow.Bifoliolate
do you remember about mode="TransportCredentialOnly" from link in your first update? Do you have it in your security node?Whispering
G
11

What you need is to send a username token over http transport which is not supported in wcf ootb. in addition your token uses nonce/created which is also not ootb. you have 2 options:

  1. this oss project adds nonce/created to the username token. this oss project adds the ability to send username over http. you would need to combine both projects together.

  2. ws-security is usually considered complex, but you use it in its simplest form (username). the easiest would be to dismiss any wcf security setting all together and create the whole security header by yourself in a message inspector! As you can see most headers are just static xml nodes, and most values are pretty clear (you know the username). the only tricky two are the nonce and the timestamps which you could look how to do in this oss project (one line each). There is a variant of this option which may be easier - use CUB after all and implement a custom encoder which pushes the timestmpa/nonce. I would go for the latter but I'm biased since I developed CUB...

There's also the ws-addressing headers which you can configure on your custom encoding "messageVersion" property. I can't tell the exact value since you omitted the envelope header with the wsa prefix definition.

If you want help privately (since you seem to have security restrictions) by all means send me an email from my blog.

EDIT: I've implemented it for you. follow these steps:

  1. download cub and make yourself familiar with it (not the internals, just how to use it according to the blog post)

  2. add reference to System.Runtime.Serialization.dll to the project ClearUsernameBinding

  3. add a new file to that project: UsernameExEncoder.cs. Paste this content:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ServiceModel.Channels;
    using System.IO;
    using System.Xml;
    using System.Security.Cryptography;
    
    namespace Webservices20.BindingExtensions
       {
        class UsernameExEncoderBindingElement : MessageEncodingBindingElement
        {
        MessageEncodingBindingElement inner;        
    
        public UsernameExEncoderBindingElement(MessageEncodingBindingElement inner)
        {
            this.inner = inner;            
        }
    
        public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
        {
            context.BindingParameters.Add(this);
            var res = base.BuildChannelFactory<TChannel>(context);
            return res;
        }
    
        public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
        {
            var res = base.CanBuildChannelFactory<TChannel>(context);
            return res;
        }
    
        public override MessageEncoderFactory CreateMessageEncoderFactory()
        {
            return new UsernameExEncoderFactory(this.inner.CreateMessageEncoderFactory());
        }      
    
        public override MessageVersion MessageVersion
        {
            get
            {
                return this.inner.MessageVersion;
            }
            set
            {
                this.inner.MessageVersion = value;
            }
        }
    
        public override BindingElement Clone()
        {
            var c = (MessageEncodingBindingElement)this.inner.Clone();
            var res = new UsernameExEncoderBindingElement(c);
            return res;
        }
    
        public override T GetProperty<T>(BindingContext context)
        {
            var res = this.inner.GetProperty<T>(context);
            return res;
        }
    }
    
    class UsernameExEncoderFactory : MessageEncoderFactory
    {
        MessageEncoderFactory inner;        
    
        public UsernameExEncoderFactory(MessageEncoderFactory inner)
        {
            this.inner = inner;            
        }
    
        public override MessageEncoder Encoder
        {
            get { return new UsernameExEncoder(inner.Encoder); }
        }
    
        public override MessageVersion MessageVersion
        {
            get { return this.inner.MessageVersion; }
        }
    
    }
    
    class UsernameExEncoder : MessageEncoder
    {
        MessageEncoder inner;
    
        public override T GetProperty<T>()
        {
            return inner.GetProperty<T>();
        }
    
        public UsernameExEncoder(MessageEncoder inner)
        {
            this.inner = inner;
        }
    
        public override string ContentType
        {
            get { return this.inner.ContentType; }
        }
    
        public override string MediaType
        {
            get { return this.inner.MediaType; }
        }
    
        public override MessageVersion MessageVersion
        {
            get { return this.inner.MessageVersion; }
        }
    
        public override bool IsContentTypeSupported(string contentType)
        {
            return this.inner.IsContentTypeSupported(contentType);
        } 
    
        public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
        {
            return this.inner.ReadMessage(buffer, bufferManager, contentType);
        }
    
        public override Message ReadMessage(System.IO.Stream stream, int maxSizeOfHeaders, string contentType)
        {
            return this.inner.ReadMessage(stream, maxSizeOfHeaders, contentType);
        }
    
        public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
        {   
            //load the message to dom
            var mem = new MemoryStream();
            var x = XmlWriter.Create(mem);
            message.WriteMessage(x);
            x.Flush();
            mem.Flush();
            mem.Position = 0;
            XmlDocument doc = new XmlDocument();
            doc.Load(mem);
    
            //add the missing elements
            var token = doc.SelectSingleNode("//*[local-name(.)='UsernameToken']");
            var created = doc.CreateElement("Created", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
            var nonce = doc.CreateElement("Nonce", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
            token.AppendChild(created);
            token.AppendChild(nonce);
    
            //set nonce value
            byte[] nonce_bytes = new byte[16];
            RandomNumberGenerator rndGenerator = new RNGCryptoServiceProvider();
            rndGenerator.GetBytes(nonce_bytes);
            nonce.InnerText = Convert.ToBase64String(nonce_bytes);
    
            //set create value
            created.InnerText = XmlConvert.ToString(DateTime.Now.ToUniversalTime(), "yyyy-MM-ddTHH:mm:ssZ");
    
            //create a new message
            var r = XmlReader.Create(new StringReader(doc.OuterXml));
            var newMsg = Message.CreateMessage(message.Version, message.Headers.Action, r);
    
            return this.inner.WriteMessage(newMsg, maxMessageSize, bufferManager, messageOffset);
        }
    
    
    
    
        public override void WriteMessage(Message message, System.IO.Stream stream)
        {
            this.inner.WriteMessage(message, stream);
        }
    }
    }
    
  4. In the file ClearUsernameBinding.cs replace this:

    res.Add(new TextMessageEncodingBindingElement() { MessageVersion = this.messageVersion});

    with this:

    var textEncoder = new TextMessageEncodingBindingElement() { MessageVersion = this.messageVersion }; res.Add(new UsernameExEncoderBindingElement(textEncoder));

  5. In the project TestClient in app.config there is a messageVersion property on the binding element. You have not published the root of your envelope so I cannot know for sure, but probably you need to set it to Soap11WSAddressingAugust2004 or Soap11WSAddressing10 (or one of these with Soap12 instead).

Good luck!

Galvin answered 19/5, 2012 at 0:58 Comment(2)
Yaron--I'm getting a proxy violation here on the work network trying to get the CUB code (because it's @ blogspot). Do you have any other locations where the code is available?Bifoliolate
the code is in google code code.google.com/p/wcf-clear-username-binding the blog just contains the explanationGalvin

© 2022 - 2024 — McMap. All rights reserved.