Digest Authentication in System.Net classes don't fully comply with RFC2617
Symptoms
Using a class from the System.Net namespace, e.g. HttpWebRequest, to authenticate to a non-Microsoft web server using Digest authentication might result in some error condition, e.g. an HTTP 500 error:
HTTP/1.1 500 Internal Server Error
Date: Fri, 30 Nov 2012 12:15:15 GMT
Server: Apache/2.2.22 (Win32)
Content-Length: 547
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>500 Internal Server Error</title>
</head><body>
< h1>Internal Server Error</h1>
<p>The server encountered an internal error or
misconfiguration and was unable to complete
your request.</p>
</body></HTML>
Cause
System.Net classes don't include the query string in the 'Uri' attribute of the digest authentication header. This is a violation of the RFC, and some web server implementations reject those requests.
Resolution
The below workaround provides an implementation of the Digest authentication protocol which generates an Authentication header compliant with the RFC2617. The below code sample is based on the sample provided originally in the following forum: https://stackoverflow.com/questions/3109507/httpwebrequests-sends-parameterless-uri-in-authorization-header.
IMPORTANT: This sample code is provided as-is and is intended for sample purposes only. It is provided without warranties and confers no rights.
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace DigestClient
{
publicclassDigestHttpWebRequest
{
privatestring _user;
privatestring _password;
privatestring _realm;
privatestring _nonce;
privatestring _qop;
privatestring _cnonce;
privateAlgorithm _md5;
privateDateTime _cnonceDate;
privateint _nc;
privatestring _requestMethod = WebRequestMethods.Http.Get;
privatestring _contentType;
privatebyte[] _postData;
public DigestHttpWebRequest(string user, string password)
{
_user = user;
_password = password;
}
publicstring Method
{
get { return _requestMethod; }
set { _requestMethod = value; }
}
publicstring ContentType
{
get { return _contentType; }
set { _contentType = value; }
}
publicbyte[] PostData
{
get { return _postData; }
set { _postData = value; }
}
publicHttpWebResponse GetResponse(Uri uri)
{
HttpWebResponse response = null;
int infiniteLoopCounter = 0;
int maxNumberAttempts = 2;
while ((response == null ||
response.StatusCode != HttpStatusCode.Accepted) &&
infiniteLoopCounter < maxNumberAttempts)
{
try
{
var request = CreateHttpWebRequestObject(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", ComputeDigestHeader(uri));
}
try
{
response = (HttpWebResponse)request.GetResponse();
}
catch (WebException webException)
{
// Try to fix a 401 exception by adding a Authorization header
if (webException.Response != null &&
((HttpWebResponse)webException.Response).StatusCode == HttpStatusCode.Unauthorized)
{
var wwwAuthenticateHeader = webException.Response.Headers["WWW-Authenticate"];
_realm = GetDigestHeaderAttribute("realm", wwwAuthenticateHeader);
_nonce = GetDigestHeaderAttribute("nonce", wwwAuthenticateHeader);
_qop = GetDigestHeaderAttribute("qop", wwwAuthenticateHeader);
_md5 = GetMD5Algorithm(wwwAuthenticateHeader);
_nc = 0;
_cnonce = newRandom().Next(123400, 9999999).ToString();
_cnonceDate = DateTime.Now;
request = CreateHttpWebRequestObject(uri, true);
infiniteLoopCounter++;
response = (HttpWebResponse)request.GetResponse();
}
else
{
throw webException;
}
}
switch (response.StatusCode)
{
caseHttpStatusCode.OK:
caseHttpStatusCode.Accepted:
return response;
caseHttpStatusCode.Redirect:
caseHttpStatusCode.Moved:
uri = newUri(response.Headers["Location"]);
// We decrement the loop counter, as there might be a variable number of redirections which we should follow
infiniteLoopCounter--;
break;
}
}
catch (WebException ex)
{
throw ex;
}
}
thrownewException("Error: Either authentication failed, authorization failed or the resource doesn't exist");
}
privateHttpWebRequest CreateHttpWebRequestObject(Uri uri, bool addAuthenticationHeader)
{
var request = (HttpWebRequest)WebRequest.Create(uri);
request.AllowAutoRedirect = false;
request.Method = this.Method;
if (!String.IsNullOrEmpty(this.ContentType))
{
request.ContentType = this.ContentType;
}
if (addAuthenticationHeader)
{
request.Headers.Add("Authorization", ComputeDigestHeader(uri));
}
if (this.PostData != null && this.PostData.Length > 0)
{
request.ContentLength = this.PostData.Length;
Stream postDataStream = request.GetRequestStream(); //open connection
postDataStream.Write(this.PostData, 0, this.PostData.Length); // Send the data.
postDataStream.Close();
}
elseif (
this.Method == WebRequestMethods.Http.Post &&
(this.PostData == null || this.PostData.Length == 0))
{
request.ContentLength = 0;
}
return request;
}
privateHttpWebRequest CreateHttpWebRequestObject(Uri uri)
{
return CreateHttpWebRequestObject(uri, false);
}
privatestring ComputeDigestHeader(Uri uri)
{
_nc = _nc + 1;
string ha1, ha2;
switch (_md5)
{
caseAlgorithm.MD5sess:
var secret = ComputeMd5Hash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", _user, _realm, _password));
ha1 = ComputeMd5Hash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", secret, _nonce, _cnonce));
ha2 = ComputeMd5Hash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", this.Method, uri.PathAndQuery));
var data = string.Format(CultureInfo.InvariantCulture, "{0}:{1:00000000}:{2}:{3}:{4}",
_nonce,
_nc,
_cnonce,
_qop,
ha2);
var kd = ComputeMd5Hash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", ha1, data));
returnstring.Format("Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", " +
"algorithm=MD5-sess, response=\"{4}\", qop={5}, nc={6:00000000}, cnonce=\"{7}\"",
_user, _realm, _nonce, uri.PathAndQuery, kd, _qop, _nc, _cnonce);
caseAlgorithm.MD5:
ha1 = ComputeMd5Hash(string.Format("{0}:{1}:{2}", _user, _realm, _password));
ha2 = ComputeMd5Hash(string.Format("{0}:{1}", this.Method, uri.PathAndQuery));
var digestResponse =
ComputeMd5Hash(string.Format("{0}:{1}:{2:00000000}:{3}:{4}:{5}", ha1, _nonce, _nc, _cnonce, _qop, ha2));
returnstring.Format("Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", " +
"algorithm=MD5, response=\"{4}\", qop={5}, nc={6:00000000}, cnonce=\"{7}\"",
_user, _realm, _nonce, uri.PathAndQuery, digestResponse, _qop, _nc, _cnonce);
}
thrownewException("The digest header could not be generated");
}
privatestring GetDigestHeaderAttribute(string attributeName, string digestAuthHeader)
{
var regHeader = newRegex(string.Format(@"{0}=""([^""]*)""", attributeName));
var matchHeader = regHeader.Match(digestAuthHeader);
if (matchHeader.Success)
return matchHeader.Groups[1].Value;
thrownewApplicationException(string.Format("Header {0} not found", attributeName));
}
privateAlgorithm GetMD5Algorithm(string digestAuthHeader)
{
var md5Regex = newRegex(@"algorithm=(?<algo>.*)[,]", RegexOptions.IgnoreCase);
var md5Attribute = md5Regex.Match(digestAuthHeader);
if (md5Attribute.Success)
{
char[] charSeparator = newchar[] { ',' };
string algorithm = md5Attribute.Result("${algo}").ToLower().Split(charSeparator)[0];
switch (algorithm)
{
case"md5-sess":
case"\"md5-sess\"":
returnAlgorithm.MD5sess;
case"md5":
case"\"md5\"":
default:
returnAlgorithm.MD5;
}
}
thrownewApplicationException("Could not determine Digest algorithm to be used from the server response.");
}
privatestring ComputeMd5Hash(string input)
{
var inputBytes = Encoding.ASCII.GetBytes(input);
var hash = MD5.Create().ComputeHash(inputBytes);
var sb = newStringBuilder();
foreach (var b in hash)
sb.Append(b.ToString("x2"));
return sb.ToString();
}
publicenumAlgorithm
{
MD5 = 0, // Apache Default
MD5sess = 1 //IIS Default
}
}
}
Sample usage for HTTP GET requests:
DigestHttpWebRequest request = newDigestHttpWebRequest(username, password);
HttpWebResponse result = request.GetResponse(uri);
Sample usage for HTTP POST requests:
DigestHttpWebRequest request = newDigestHttpWebRequest(username, password);
request.Method = WebRequestMethods.Http.Post;
request.ContentType = "text/plain; charset=utf-8";
request.PostData = Encoding.ASCII.GetBytes(postData);
HttpWebResponse result = request.GetResponse(uri);
Applies to Microsoft .NET Framework 1.1
Microsoft .NET Framework 2.0
Microsoft .NET Framework 3.0
Microsoft .NET Framework 3.5
Microsoft .NET Framework 4.0
Microsoft .NET Framework 4.5