HttpWebRequests sends parameterless URI in Authorization header
Asked Answered
S

3

6

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?

Starcrossed answered 24/6, 2010 at 11:31 Comment(4)
What URI are you using to create the web request? Does it contain the "query?id=1" part?Thermy
Also, can you get a wireshark trace of a successful request from the browser? And then compare the two. I suspect it might not have anything to do with the auth header. If the auth header was not correct you would have received a 401 response (not 400)Thermy
@feroze: Both the headers in the questions are from Wireshark. If this particular server thinks the URI in the authorization header is incorrect, it returns 400 instead of 401Starcrossed
Having a look at the Reference Source, it seems the query part has been removed deliberately because "it breaks IIS6": referencesource.microsoft.com/#System/net/System/Net/…Crispi
S
12

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();
    }
}
Starcrossed answered 25/6, 2010 at 9:50 Comment(3)
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: connect.microsoft.com/VisualStudio/feedback/details/571052/…Thermy
This answer can be improved by making it thread safe (instead of using shared static state), disposing of IDisposable objects correctly, and other FxCop-compliance fixes.Colossal
If anyone else is looking for a modernised answer with a full implementation, I made a blog post to show how it can be done 'from scratch': dev.to/callumhoughton18/…Extinction
A
7

I ran into this issue recently. I couldn't get the workaround from Andomar to work either without some minor adjustments. I submitted the changes as a suggestion to Andomar's answer, but they were unceremoniously rejected by TheTinMan and Lucifer. Since it took me and some collegues hours to figure these out and I'm sure someone else will need this I'm posting the code as an answer to make it available.

Here's the adjusted code. Basically an "opaque" header variable was needed, and some quotes needed fixing in GetDigestHeader.

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 string _opaque;
    private static DateTime _cnonceDate;
    private static int _nc = 0;

    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}\", opaque=\"{8}\"",
        _user, _realm, _nonce, dir, digestResponse, _qop, _nc, _cnonce, _opaque);
    }

    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);
            _opaque = GrabHeaderVar("opaque", 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();
    }
}
Ariana answered 16/4, 2012 at 18:59 Comment(2)
I agree. Your fixes were absolutely necessary.Tubercular
Any case you know why i get the WWW-Authenticate" as null?Froe
U
0

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.

Undersea answered 24/6, 2010 at 11:50 Comment(1)
That looks like a fix for BizTalk. We're not using BizTalk, and I can set KeepAlive and PreAuthenticate to true: same resultStarcrossed

© 2022 - 2024 — McMap. All rights reserved.