22

Various articles (1, 2) I discovered make this look easy enough:

WebRequest request = HttpWebRequest.Create(url);

var credentialCache = new CredentialCache();
credentialCache.Add(
  new Uri(url), // request url
  "Digest", // authentication type
  new NetworkCredential("user", "password") // credentials
);

request.Credentials = credentialCache;

However, this only works for URLs without URL parameters. For example, I can download http://example.com/test/xyz.html just fine, but when I attempt to download http://example.com/test?page=xyz, the result is a 400 Bad Request message with the following in the server's logs (running Apache 2.2):

Digest: uri mismatch - </test> does not match request-uri </test?page=xyz>

My first idea was that the digest specification requires URL parameters to be removed from the digest hash -- but removing the parameter from the URL passed to credentialCache.Add() didn't change a thing. So it must be the other way around and somewhere in the .NET framework is wrongly removing the parameter from the URL.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Cygon
  • 9,444
  • 8
  • 42
  • 50
  • Here's a similar question on SO my initial search didn't come up with: http://stackoverflow.com/questions/3109507/httpwebrequests-sends-parameterless-uri-in-authorization-header – Cygon Jul 03 '10 at 19:08
  • And a Microsoft Connect bug report: https://connect.microsoft.com/VisualStudio/feedback/details/571052/digest-authentication-does-not-send-the-full-uri-path-in-the-uri-parameter – Cygon Jul 03 '10 at 19:09
  • The Microsoft Connect bug report linked above seems to have a workaround, posted 6/26. Have you tried that? – Samuel Meacham Jul 17 '10 at 01:39
  • Yes, that would solve it. However, it's really a workaround in the sense that it reimplements functionality of the .NET Framework. I was hoping that it was just a mistake in my usage of the HttpWebRequest class. – Cygon Jul 21 '10 at 08:18
  • 1
    There's even a hack in Apache's mod_auth_digest (the module that performs digest authentication) to work around this very same issue happening with Internet Explorer: http://httpd.apache.org/docs/2.0/mod/mod_auth_digest.html#msie – Cygon Jul 21 '10 at 08:23

5 Answers5

11

Code taken from this post has worked perfectly for me Implement Digest authentication via HttpWebRequest in C#

I had following issue, when ever I browser the feed url in a browser it asked for username and password and worked fine, however any of the above code samples were not working, on inspecting Request/Response Header (in web developer tools in firefox) i could see header having Authorization of type digest.

Step 1 Add:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Net;
using System.IO;

namespace NUI
{
    public class DigestAuthFixer
    {
        private static string _host;
        private static string _user;
        private static string _password;
        private static string _realm;
        private static string _nonce;
        private static string _qop;
        private static string _cnonce;
        private static DateTime _cnonceDate;
        private static int _nc;

    public DigestAuthFixer(string host, string user, string password)
    {
        // TODO: Complete member initialization
        _host = host;
        _user = user;
        _password = password;
    }

    private 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 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));
    }

    private 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 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();
    }
}

}

Step 2: Call new method

DigestAuthFixer digest = new DigestAuthFixer(domain, username, password);
string strReturn = digest.GrabResponse(dir);

if Url is: http://xyz.rss.com/folder/rss then domain: http://xyz.rss.com (domain part) dir: /folder/rss (rest of the url)

you could also return it as stream and use XmlDocument Load() method.

Community
  • 1
  • 1
Mvg Developer
  • 183
  • 1
  • 7
9

You said you removed the querystring paramters, but did you try going all the way back to just the host? Every single example of CredentialsCache.Add() I've seen seems to use only the host, and the docs for CredentialsCache.Add() list the Uri parameter as "uriPrefix", which seems telling.

In other words, try this out:

Uri uri = new Uri(url);
WebRequest request = WebRequest.Create(uri);

var credentialCache = new CredentialCache(); 
credentialCache.Add( 
  new Uri(uri.GetLeftPart(UriPartial.Authority)), // request url's host
  "Digest",  // authentication type 
  new NetworkCredential("user", "password") // credentials 
); 

request.Credentials = credentialCache;

If this works, you will also have to make sure that you don't add the same "authority" to the cache more than once... all requests to the same host should be able to make use of the same credential cache entry.

JaredReisinger
  • 6,955
  • 1
  • 22
  • 21
  • Strange, I didn't come across a single example using just the root URI for authentication. In any case, it doesn't work, sorry. As per section 3.2.2 of RFC 2617 (http://rfc.askapache.com/rfc2617/rfc2617.html#section-3.2.2) the digest URI should be identical to the 'request-uri' in the HTTP request. – Cygon Jul 21 '10 at 08:30
  • Here are some examples: http://msdn.microsoft.com/en-us/library/system.net.credentialcache.aspx, http://support.microsoft.com/kb/822456, http://blogs.msdn.com/b/buckh/archive/2004/07/28/199706.aspx (though admittedly it's a 'localhost' example). – JaredReisinger Jul 21 '10 at 21:50
  • Yes, the RFC says that the digest-uri should match the request, but that's what is sent on the wire, not what's stored in the cache. The CredentialCache.GetCredential() doc (http://msdn.microsoft.com/en-us/library/fy4394xd.aspx) says that "GetCredential uses the longest matching URI prefix in the cache to determine which set of credentials to return for an authorization type." It then shows that passing a domain will cause the credentials to get used for *all* resources under that domain. – JaredReisinger Jul 21 '10 at 21:51
  • I see, you're right, the 'digest-uri' sent to the server for authentication of a single request has no relation to the URI stored in the credential cache. So in production code, one can store a higher level URI like `http://example.com/restrictedarea/` in the credential cache and reuse the cache for all URIs below that path. – Cygon Jul 23 '10 at 21:38
  • Anyway, this doesn't affect the problem. The credentials are successfully looked up in the cache (if that's your worry) and the HttpWebRequest does attempt to authenticate against my web server (as can be seen in the server logs) - but with the wrong 'digest-uri' whenever the requested URL has parameters appended to it. – Cygon Jul 23 '10 at 22:16
  • Ah hah... sounds like a bug in the HttpWebRequest credentials code. So it is really the Microsoft Connect bug mentioned in the comments on your question (https://connect.microsoft.com/VisualStudio/feedback/details/571052/digest-authentication-does-not-send-the-full-uri-path-in-the-uri-parameter). That sucks. – JaredReisinger Jul 24 '10 at 00:13
  • Works great when used with Axis speakers API's. Obviously store credentials someplace safe and pass in as params. Great simple solution. – robnick Apr 13 '21 at 03:49
1

In earlier answers everybody use the obsolete WEbREquest.Create method. So here is my async solution what up to date for recently trending's:

public async Task<string> LoadHttpPageWithDigestAuthentication(string url, string username, string password)
    {
        Uri myUri = new Uri(url);
        NetworkCredential myNetworkCredential = new NetworkCredential(username, password);
        CredentialCache myCredentialCache = new CredentialCache { { myUri, "Digest", myNetworkCredential } };
        var request = new HttpClient(new HttpClientHandler() { Credentials = myCredentialCache, PreAuthenticate = true});

        var response = await request.GetAsync(url);

        var responseStream = await response.Content.ReadAsStreamAsync();

        StreamReader responseStreamReader = new StreamReader(responseStream, Encoding.Default);

        string answer = await responseStreamReader.ReadToEndAsync();

        return answer;
    }
Birek
  • 85
  • 6
1

The solution is to activate this parameter in apache:

    BrowserMatch "MSIE" AuthDigestEnableQueryStringHack=On 


More info : http://httpd.apache.org/docs/2.0/mod/mod_auth_digest.html#msie

Then add this property in your code for the webrequest object:

    request.UserAgent = "MSIE"

it work very well for me

drmed
  • 11
  • 2
  • Yep, see my own comment on the original question from July, 21, 2010. It's only an option when you've got control over the server and it irks me a bit that my app has to identify itself as MSIE, though ;) – Cygon Jan 05 '12 at 13:15
0

I think the second URL points to dynamic page and you should first call it using GET to get the HTML and then to download it. No experience in this field though.

Umair A.
  • 6,690
  • 20
  • 83
  • 130
  • Sorry, no. It's completely up to the web server what to do with the URL and the first page could be dynamic as well. Further, the HTML is what is downloaded, there's no difference between downloading HTML or downloading something else. – Cygon Jul 19 '10 at 07:13