0

I am developing a ONVIF driver using .NET 4 (Windows Forms, not WCF). I started importing WSDL files as a service in visual studio. So I am able to send command to a device in this way:

HttpTransportBindingElement httpTransportBindingElement = new HttpTransportBindingElement();
[...]

TextMessageEncodingBindingElement messegeElement = new TextMessageEncodingBindingElement();
[...]
CustomBinding binding = new CustomBinding(messegeElement, httpTransportBindingElement);
[...]

EndpointAddress serviceAddress = new EndpointAddress(url);

DeviceClient deviceClient = new DeviceClient(binding, serviceAddress);

Device channel = deviceClient.ChannelFactory.CreateChannel();

DeviceServiceCapabilities dsc = channel.GetServiceCapabilities();

But I am not able to manage HTTP digest authentication. I spent days searching on google examples and solutions, but the only ways seems to be hand write XML code. There is not any clean solution like:

deviceClient.ChannelFactory.Credentials.HttpDigest.ClientCredential.UserName = USERNAME;
deviceClient.ChannelFactory.Credentials.HttpDigest.ClientCredential.Password = digestPassword;

(that doesn't work)?

3 Answers3

1

First of all you should install Microsoft.Web.Services3 package. (View> Other windows> Package manager console). Then you must add digest behavior to your endpoint. The first part of the code is PasswordDigestBehavior class and after that it is used for connecting to an ONVIF device service.

public class PasswordDigestBehavior : IEndpointBehavior
{
    public String Username { get; set; }
    public String Password { get; set; }

    public PasswordDigestBehavior(String username, String password)
    {
        this.Username = username;
        this.Password = password;
    }


    public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
        // do nothing
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
    {
        //clientRuntime.MessageInspectors.Add(new PasswordDigestMessageInspector(this.Username, this.Password));
        clientRuntime.MessageInspectors.Add(new PasswordDigestMessageInspector(this.Username, this.Password));
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
    {
        throw new NotImplementedException();
    }

    public void Validate(ServiceEndpoint endpoint)
    {
        // do nothing...
    }
}


public class PasswordDigestMessageInspector : IClientMessageInspector
{
    public String Username { get; set; }
    public String Password { get; set; }

    public PasswordDigestMessageInspector(String username, String password)
    {
        this.Username = username;
        this.Password = password;
    }

    public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
    {
        // do nothing
    }

    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
    {
        // Use the WSE 3.0 security token class
        var option = PasswordOption.SendHashed;
        if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
            option = PasswordOption.SendPlainText;

        UsernameToken token = new UsernameToken(this.Username, this.Password, option);

        // Serialize the token to XML
        XmlDocument xmlDoc = new XmlDocument();
        XmlElement securityToken = token.GetXml(xmlDoc);

        // find nonce and add EncodingType attribute for BSP compliance
        XmlNamespaceManager nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
        nsMgr.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        XmlNodeList nonces = securityToken.SelectNodes("//wsse:Nonce", nsMgr);
        XmlAttribute encodingAttr = xmlDoc.CreateAttribute("EncodingType");
        encodingAttr.Value = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary";
        if (nonces.Count > 0)
        {
            nonces[0].Attributes.Append(encodingAttr);
            //nonces[0].Attributes[0].Value = "foo";
        }


        //
        MessageHeader securityHeader = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", securityToken, false);
        request.Headers.Add(securityHeader);

        // complete
        return Convert.DBNull;
    }
}

And this is how to use it:

var endPointAddress = new EndpointAddress("http://DEVICE_IPADDRESS/onvif/device_service");
            var httpTransportBinding = new HttpTransportBindingElement { AuthenticationScheme = AuthenticationSchemes.Digest };
            var textMessageEncodingBinding = new TextMessageEncodingBindingElement { MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.None) };
            var customBinding = new CustomBinding(textMessageEncodingBinding, httpTransportBinding);
            var passwordDigestBehavior = new PasswordDigestBehavior(USERNAME, PASSWORD);
            var deviceService = new DeviceClient(customBinding, endPointAddress);
            deviceService.Endpoint.Behaviors.Add(passwordDigestBehavior);
aminexplo
  • 360
  • 1
  • 13
  • Your solution is working, but is performing SOAP authentication and not HTTP Digest Authentication. Anyway finally I was able to perform both type of authentication without using WSE 3.0. –  May 11 '17 at 13:32
0

For future readers, finally I was able to perform both type of authentication without using WSE 3.0. This is partial code (for shortness), based on the IClientMessageInspector interface (you can find lot of other examples based on this interface):

    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
    {
        if (HTTPDigestAuthentication)
        {
            string digestHeader = string.Format("Digest username=\"{0}\",realm=\"{1}\",nonce=\"{2}\",uri=\"{3}\"," +
                                                "cnonce=\"{4}\",nc={5:00000000},qop={6},response=\"{7}\",opaque=\"{8}\"",
                                                _username, realm, nonce, new Uri(this.URI).AbsolutePath, cnonce, counter, qop, digestResponse, opaque);

            HttpRequestMessageProperty httpRequest = new HttpRequestMessageProperty();
            httpRequest.Headers.Add("Authorization", digestHeader);
            request.Properties.Add(HttpRequestMessageProperty.Name, httpRequest);

            return Convert.DBNull;
        }
        else if (UsernametokenAuthorization)
        {
            string headerText = "<wsse:UsernameToken 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\">" +
                                "<wsse:Username>" + _username + "</wsse:Username>" +
                                "<wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">" + digestPassword + "</wsse:Password>" +
                                "<wsse:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">" + Convert.ToBase64String(nonce) + "</wsse:Nonce>" +
                                "<wsu:Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">" + created + "</wsu:Created>" +
                                "</wsse:UsernameToken>";

            XmlDocument MyDoc = new XmlDocument();
            MyDoc.LoadXml(headerText);

            MessageHeader myHeader = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", MyDoc.DocumentElement, false);

            request.Headers.Add(myHeader);

            return Convert.DBNull;
        }

        return request;
    }
  • A lot of things are missing from your example, and some very important things... How do you get the variables that are used to format your digest header? (for example nonce? do you need to make a request for that?) – cube45 Aug 21 '17 at 08:45
  • Yes you are right: you have to send a request without authentication, the server will answer with an error message containing nonce, realm and so on. –  Aug 28 '17 at 09:13
  • Are you making your own HttpClient at the beginning of BeforeSendRequest? Could you edit your answer and provide a more complete example? – cube45 Aug 28 '17 at 09:17
  • Look at this link for a more complete example to start from: http://findnerd.com/list/view/How-to-resolve-Http-400-Bad-Request-while-authenticating-with-ONVIF-camera/22324/ –  Aug 28 '17 at 10:09
  • Yeah, I found that Internet is full of examples about using the usernametoken mechanism. I'm using it and it works pretty well. However, I was curious about your "HttpDigest" mechanism. Onvif's spec now states that "The services defined in this standard shall be protected using digest authentication according to [RFC 2617] with the exception of legacy devices supporting [WS-UsernameToken]." It makes me think that WS-UsernameToken is a thing of the past(for ONVIF). Moreover, you are the only example of http digest authentication with service reference I found on internet that is not for AD. – cube45 Aug 28 '17 at 13:56
  • I agree with you, I wasted lot of time to make it working as there are not examples using onvif service references. Have you got it working? If not, I will try to edit the answer providing more code. –  Aug 28 '17 at 15:09
  • I know I'm late for the party, but could you provide more code?? It would be really usefull! The cameras I'm working on seem to support the usernameToken authentication and I can't make it work... You can see some of my work on this link: https://stackoverflow.com/questions/50220574/how-to-generate-getsystemdateandtime-xml – LoukMouk May 09 '18 at 15:16
  • Hi LoukMo, have you checked the code at this link? It was my start point http://findnerd.com/list/view/How-to-resolve-Http-400-Bad-Request-while-authenticating-with-ONVIF-camera/22324/ –  May 11 '18 at 12:14
0

This class should be able to replace the WSE UserNameToken object and remove the dependency on WSE. It also makes the searching and repairing nonces in IClientInspector unnecessary. I've only tested it on 1 camera and only with hashed passwords. YMMV.

public enum PasswordOption
{
    SendPlain = 0,
    SendHashed = 1,
    SendNone = 2
}

public class UsernameToken
{
    private string Username;
    private string Password;
    private PasswordOption PwdOption;

    public UsernameToken(string username, string password, PasswordOption option)
    {
        Username = username;
        Password = password;
        PwdOption = option;
    }


    public XmlElement GetXml(XmlDocument xmlDoc)
    {
        string wsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
        string wsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
        XmlDocument doc = xmlDoc;
        //XmlElement securityEl = doc.CreateElement("Security", wsse);

        XmlElement usernameTokenEl = doc.CreateElement("wsse", "UsernameToken", wsse);
        XmlAttribute a = doc.CreateAttribute("wsu", "Id", wsu);
        usernameTokenEl.SetAttribute("xmlns:wsse", wsse);
        usernameTokenEl.SetAttribute("xmlns:wsu", wsu);
        a.InnerText = "SecurityToken-" + Guid.NewGuid().ToString();
        usernameTokenEl.Attributes.Append(a);

        //Username
        XmlElement usernameEl = doc.CreateElement("wsse:Username", wsse);
        usernameEl.InnerText = Username;
        usernameTokenEl.AppendChild(usernameEl);

        //Password
        XmlElement pwdEl = doc.CreateElement("wsse:Password", wsse);


        switch (PwdOption)
            {
            case PasswordOption.SendHashed:
                //Nonce+Create+Password
                pwdEl.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
                string created = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
                byte[] nonce = GenerateNonce(16);
                byte[] pwdBytes = Encoding.ASCII.GetBytes(Password);
                byte[] createdBytes = Encoding.ASCII.GetBytes(created);
                byte[] pwdDigest = new byte[nonce.Length + pwdBytes.Length + createdBytes.Length];
                Array.Copy(nonce, pwdDigest, nonce.Length);
                Array.Copy(createdBytes, 0, pwdDigest, nonce.Length, createdBytes.Length);
                Array.Copy(pwdBytes, 0, pwdDigest, nonce.Length + createdBytes.Length, pwdBytes.Length);
                pwdEl.InnerText = ToBase64(SHA1Hash(pwdDigest));
                usernameTokenEl.AppendChild(pwdEl);

                //Nonce
                XmlElement nonceEl = doc.CreateElement("wsse:Nonce", wsse);
                nonceEl.SetAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
                nonceEl.InnerText = ToBase64(nonce);
                usernameTokenEl.AppendChild(nonceEl);

                //Created
                XmlElement createdEl = doc.CreateElement("wsu:Created", wsu);
                createdEl.InnerText = created;
                usernameTokenEl.AppendChild(createdEl);
                break;
            case PasswordOption.SendNone:
                pwdEl.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText");
                pwdEl.InnerText = "";
                usernameTokenEl.AppendChild(pwdEl);
                break;
            case PasswordOption.SendPlain:
                pwdEl.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText");
                pwdEl.InnerText = Password;
                usernameTokenEl.AppendChild(pwdEl);
                break;
        }

        return usernameTokenEl;
    }

    private byte[] GenerateNonce(int bytes)
    {
        byte[] output = new byte[bytes];
        Random r = new Random(DateTime.Now.Millisecond);
        r.NextBytes(output);
        return output;
    }

    private static byte[] SHA1Hash(byte[] input)
    {
        SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
        return sha1Hasher.ComputeHash(input);
    }

    private static string ToBase64(byte[] input)
    {
        return Convert.ToBase64String(input);
    }
}

}