views:

84

answers:

2

I'm connecting to a web service from .NET, like:

var request = (HttpWebRequest) WebRequest.Create(uri);
request.Credentials = new NetworkCredential("usr", "pwd", "domain");
var response = (HttpWebResponse) request.GetResponse();

The authorization header looks like:

Authorization: Digest username="usr",realm="domain",nonce="...",
    uri="/dir",algorithm="MD5",etc...
    ^^^^^^^^^^

The server returns (400) Bad Request. A header send by Chrome or IE looks like:

Authorization: Digest username="usr", realm="domain", nonce="...", 
    uri="/dir/query?id=1", algorithm=MD5, etc...
    ^^^^^^^^^^^^^^^^^^^^^

We suspect that the difference in URI is causing the web service to deny the request with a 400 error. Is it possible to make HttpRequest send out an Authorization header that includes the full URI?

A: 

It looks like you need to install this hotfix might help you out:

http://support.microsoft.com/?kbid=924638

Your problem was probably happening because you weren't able to set the KeepAlive property to false when you're using the HTTP adapter to post a message

Also ensure PreAuthenticate is set to true.

Samiksha
That looks like a fix for BizTalk. We're not using BizTalk, and I can set KeepAlive and PreAuthenticate to true: same result
Andomar
+1  A: 

It turns out that Digest authentication is fairly easy to implement. With our own implementation, we were able to use the full URI (including parameters) to generate the MD5 hash. That fixed the problem.

In case someone hits this problem in the future, you can call the workaround like:

var resultText = DigestAuthFixer.GrabResponse("/dir/index.html");

The code for the DigestAuthFixer class:

public static class DigestAuthFixer
{
    private static string _host = "http://localhost";
    private static string _user = "Mufasa";
    private static string _password = "Circle Of Life";
    private static string _realm;
    private static string _nonce;
    private static string _qop;
    private static string _cnonce;
    private static DateTime _cnonceDate;
    private static int _nc;

    private static string CalculateMd5Hash(
        string input)
    {
        var inputBytes = Encoding.ASCII.GetBytes(input);
        var hash = MD5.Create().ComputeHash(inputBytes);
        var sb = new StringBuilder();
        foreach (var b in hash)
            sb.Append(b.ToString("x2"));
        return sb.ToString();
    }

    private static string GrabHeaderVar(
        string varName,
        string header)
    {
        var regHeader = new Regex(string.Format(@"{0}=""([^""]*)""", varName));
        var matchHeader = regHeader.Match(header);
        if (matchHeader.Success)
            return matchHeader.Groups[1].Value;
        throw new ApplicationException(string.Format("Header {0} not found", varName));
    }

    // http://en.wikipedia.org/wiki/Digest_access_authentication
    private static string GetDigestHeader(
        string dir)
    {
        _nc = _nc + 1;

        var ha1 = CalculateMd5Hash(string.Format("{0}:{1}:{2}", _user, _realm, _password));
        var ha2 = CalculateMd5Hash(string.Format("{0}:{1}", "GET", dir));
        var digestResponse =
            CalculateMd5Hash(string.Format("{0}:{1}:{2:00000000}:{3}:{4}:{5}", ha1, _nonce, _nc, _cnonce, _qop, ha2));

        return string.Format("Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", " +
            "algorithm=MD5, response=\"{4}\", qop={5}, nc={6:00000000}, cnonce=\"{7}\"",
            _user, _realm, _nonce, dir, digestResponse, _qop, _nc, _cnonce);
    }

    public static string GrabResponse(
        string dir)
    {
        var url = _host + dir;
        var uri = new Uri(url);

        var request = (HttpWebRequest)WebRequest.Create(uri);

        // If we've got a recent Auth header, re-use it!
        if (!string.IsNullOrEmpty(_cnonce) &&
            DateTime.Now.Subtract(_cnonceDate).TotalHours < 1.0)
        {
            request.Headers.Add("Authorization", GetDigestHeader(dir));
        }

        HttpWebResponse response;
        try
        {
            response = (HttpWebResponse)request.GetResponse();
        }
        catch (WebException ex)
        {
            // Try to fix a 401 exception by adding a Authorization header
            if (ex.Response == null || ((HttpWebResponse)ex.Response).StatusCode != HttpStatusCode.Unauthorized)
                throw;

            var wwwAuthenticateHeader = ex.Response.Headers["WWW-Authenticate"];
            _realm = GrabHeaderVar("realm", wwwAuthenticateHeader);
            _nonce = GrabHeaderVar("nonce", wwwAuthenticateHeader);
            _qop = GrabHeaderVar("qop", wwwAuthenticateHeader);

            _nc = 0;
            _cnonce = new Random().Next(123400, 9999999).ToString();
            _cnonceDate = DateTime.Now;

            var request2 = (HttpWebRequest)WebRequest.Create(uri);
            request2.Headers.Add("Authorization", GetDigestHeader(dir));
            response = (HttpWebResponse)request2.GetResponse();
        }
        var reader = new StreamReader(response.GetResponseStream());
        return reader.ReadToEnd();
    }
}
Andomar
Hmm - this sounds like a bug. I opened an issue on the Microsoft connect website. Feel free to login and add more details regarding the Uri , and operating system, .net framework version etc.here is the connect issue Uri: https://connect.microsoft.com/VisualStudio/feedback/details/571052/digest-authentication-does-not-send-the-full-uri-path-in-the-uri-parameter
feroze