27

I'm trying to connect to a web service, written in Java, but there's something I can't figure out.

Using WCF and a customBinding, almost everything seems to be fine, except one part of the SOAP message, as it's missing the Nonce and Created part nodes. Obviously I'm missing something, so if you could point me into the right direction, it'd be much appreciated.

Here's the custom binding:

<binding name="CustomHTTPBinding">
    <security includeTimestamp="false" authenticationMode="UserNameOverTransport" defaultAlgorithmSuite="Basic256" requireDerivedKeys="True"
              messageSecurityVersion="WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10">
    </security>
    <textMessageEncoding maxReadPoolSize="211" maxWritePoolSize="2132" messageVersion="Soap11"
                         writeEncoding="utf-8"/>
    <httpsTransport />
</binding>

And here's the relevant part of the message:

<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    <o:UsernameToken u:Id="uuid-c306efd1-e84c-410e-a2ad-1046b368582e-1">
        <o:Username>
            <!-- Removed-->
        </o:Username>
        <o:Password>
            <!-- Removed-->
        </o:Password>
    </o:UsernameToken>
</o:Security>

And this's how it should look:

<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
 <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-25763165">
    <wsse:Username>..</wsse:Username>
    <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">..</wsse:Password>
    <wsse:Nonce>6ApOnLn5Aq9KSH46pzzcZA==</wsse:Nonce>
    <wsu:Created>2009-05-13T18:59:23.309Z</wsu:Created>
 </wsse:UsernameToken>
</wsse:Security>

So the question is: How could I introduce the Nonce and Created elements inside the security part?

Luke Girvin
  • 13,221
  • 9
  • 64
  • 84
Adam Vigh
  • 1,260
  • 2
  • 13
  • 20
  • 1
    Did you figure out a solution to this? I'd be interested to know. – Jon Aug 05 '09 at 20:57
  • In the end we used WSE 2 to get around the issues we were having, instead of WCF. In there we added a custom policy to apply the UsernameToken to the service request and that was it I think. – Adam Vigh Aug 07 '09 at 10:04

4 Answers4

22

To create the nonce, I had to change a few things

First, added a custom binding in my config

<system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="myCustomBindingConfig">
          <security includeTimestamp="false" 
                    authenticationMode="UserNameOverTransport" 
                    defaultAlgorithmSuite="Basic256" 
                    requireDerivedKeys="true"
                    messageSecurityVersion="WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10">
          </security>
          <textMessageEncoding messageVersion="Soap11"></textMessageEncoding>
          <httpsTransport maxReceivedMessageSize="2000000000" />
        </binding>
      </customBinding>
    </bindings>
</system.serviceModel>

<client>
    <endpoint address="https://..." [other tags] 
        binding="customBinding" bindingConfiguration="OrangeLeapCustomBindingConfig"/>
</client>

Then, take this code found here: http://social.msdn.microsoft.com/Forums/en-US/wcf/thread/4df3354f-0627-42d9-b5fb-6e880b60f8ee and modify it to create the nonce (just a random hash, base-64 encoded)

protected override void WriteTokenCore(System.Xml.XmlWriter writer, System.IdentityModel.Tokens.SecurityToken token)
{
    Random r = new Random();
    string tokennamespace = "o";
    DateTime created = DateTime.Now;
    string createdStr = created.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
    string nonce = Convert.ToBase64String(Encoding.ASCII.GetBytes(SHA1Encrypt(created + r.Next().ToString())));
    System.IdentityModel.Tokens.UserNameSecurityToken unToken = (System.IdentityModel.Tokens.UserNameSecurityToken)token;
    writer.WriteRaw(String.Format(
    "<{0}:UsernameToken u:Id=\"" + token.Id + "\" xmlns:u=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">" +
    "<{0}:Username>" + unToken.UserName + "</{0}:Username>" +
    "<{0}:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText\">" +
    unToken.Password + "</{0}:Password>" +
    "<{0}:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">" +
    nonce + "</{0}:Nonce>" +
    "<u:Created>" + createdStr + "</u:Created></{0}:UsernameToken>", tokennamespace));
}

protected String ByteArrayToString(byte[] inputArray)
{
    StringBuilder output = new StringBuilder("");
    for (int i = 0; i < inputArray.Length; i++)
    {
    output.Append(inputArray[i].ToString("X2"));
    }
    return output.ToString();
}
protected String SHA1Encrypt(String phrase)
{
    UTF8Encoding encoder = new UTF8Encoding();
    SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
    byte[] hashedDataBytes = sha1Hasher.ComputeHash(encoder.GetBytes(phrase));
    return ByteArrayToString(hashedDataBytes);
}
David_001
  • 5,703
  • 4
  • 29
  • 55
Bron Davies
  • 5,930
  • 3
  • 30
  • 41
  • Thanks for the answer. I'm not in the position to give it a go at the moment, but it seems ok, so I'll accept it. – Adam Vigh Nov 16 '10 at 09:48
  • May be an old answer, but it just may have solved a problem I'm having, talking to a Java shop's web service! Thanks! The one missing piece was in the Microsoft page linked, after adding the custom behavior, the username and password have to be set (in the service.ClientCredentials.UserName field) – John T Nov 16 '11 at 20:56
  • @JohnT: I think I'm missing that one missing piece. I'm doing the following: `mhsClient.ChannelFactory.Endpoint.Behaviors.Remove(); mhsClient.ChannelFactory.Endpoint.Behaviors.Add(new CustomCredentials()); mhsClient.ClientCredentials.UserName.UserName = "username"; mhsClient.ClientCredentials.UserName.Password = "password";` and I'm getting the following error: `"Text cannot be written outside the root element"`. – Isaac Kleinman Jan 23 '14 at 22:03
  • @IsaacKleinman: I'm getting the same error. Any chance you fixed it and actually remember what you did? – Troels Larsen Aug 23 '16 at 14:01
  • 1
    The service I was tapping into didn't complain about missing the nonce, so I just left it out. I've described the details of my work [here](http://webservices20.blogspot.com/2014/06/emedny-web-services-in-net-guest-post.html). – Isaac Kleinman Aug 23 '16 at 14:12
  • 1
    @IsaacKleinman: Ah. Unfortunately I need it, but thanks for the article! I honestly didn't expect any response, and definitely not one so quick :D – Troels Larsen Aug 24 '16 at 10:31
  • For anyone with the same issue, the approach (with writer.WriteStartElement) from here worked: https://msdn.microsoft.com/en-us/library/ms731872%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 – Troels Larsen Aug 24 '16 at 11:48
12

I had the same problem. Instead of the custom token serializer I used a MessageInspector to add the correct UsernameToken in the BeforeSendRequest method. I then used a custom behavior to apply the fix.

The entire process is documented (with a demo project) in my blog post Supporting the WS-I Basic Profile Password Digest in a WCF client proxy. Alternatively, you can just read the PDF.

If you want to follow my progress through to the solution, you'll find it on StackOverflow titled, "Error in WCF client consuming Axis 2 web service with WS-Security UsernameToken PasswordDigest authentication scheme":

Community
  • 1
  • 1
Rebecca
  • 13,914
  • 10
  • 95
  • 136
8

It's worth pointing out that Rick Strahl made a blog post (which he references this question) where he explains it all quite clearly and offers solutions for both just Password and also PasswordDigest.

I post this because I found this article originally, couldn't really follow it, and found Rick's post much later. This might save some people some time.

WCF WSSecurity and WSE Nonce Authentication

Matt Kemp
  • 2,742
  • 2
  • 28
  • 38
  • The above answers leave out a few steps for SOAP beginners, this article gives a step by step way of working this out and explains every step along the way. – h8a Nov 08 '13 at 14:22
  • This is exactly what I was looking for. The accepted answer here doesn't explain where to put the code, but lead to the blogpost by Rick Strahl. Upvoting this answer. – MeanGreen Apr 10 '15 at 08:42
-1

I also had to put a UserNameHeader segment in the SOAP message header:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:bar:services" xmlns:efm="urn:bar:services">
   <soapenv:Header>
       <efm:UserNameHeader>
           <UserName>foouser</UserName>
           <Password>foopass</Password>
       </efm:UserNameHeader>
   </soapenv:Header>
   <soapenv:Body>
      <urn:GetUserList/>
   </soapenv:Body>
</soapenv:Envelope>

This was accomplished with a custom message header:

public class UserNamePasswordHeader : MessageHeader
{
    private readonly string _serviceUserEmail;
    private readonly string _serviceUserPassword;

    public UserNamePasswordHeader(string serviceUserEmail, string serviceUserPassword)
    {
        this._serviceUserEmail = serviceUserEmail;
        this._serviceUserPassword = serviceUserPassword;
    }

    public override string Name
    {
        get { return "UserNameHeader"; }
    }

    public override string Namespace
    {
        get { return "urn:bar:services"; }
    }

    protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
    {
        writer.WriteElementString("UserName", _serviceUserEmail);
        writer.WriteElementString("Password", _serviceUserPassword);
    }
}

Other tags, such as Nonce and Created, could easily be added.

The class is used as follows:

var service = new BarServiceClient();
service.ClientCredentials.ClientCertificate.Certificate = MessageSigningCertificate;

using (new OperationContextScope(service.InnerChannel))
{
    OperationContext.Current.OutgoingMessageHeaders.Add(
      new UserNamePasswordHeader(serviceUserEmail, serviceUserPassword));

    try
    {
        var response = service.GetUserList();
        return response;
    }
    finally
    {
        service.Close();
    }
}

Note: MessageSigningCertificate is an X.509 certificate, I read it from a file:

private static X509Certificate2 LoadCertificateFromFile(string pfxFilePath, string privateKeyPassword)
{
    // Load the certificate from a file, specifying the password
    var certificate = new X509Certificate2(pfxFilePath, privateKeyPassword);
    return certificate;
}
Handcraftsman
  • 6,863
  • 2
  • 40
  • 33