Search code examples
c#httphttpclientflurl

HttpClientHandler RFC 7616 Digest Authentication Header Using Wrong Uri


I'm trying to access a resource on a Lighttpd server which enforces that the full request URI matches the URI in the Authorization request header. This is specified in RFC 7616

The authenticating server MUST assure that the resource designated by the "uri" parameter is the same as the resource specified in the Request-Line; if they are not, the server SHOULD return a 400 Bad Request error. (Since this may be a symptom of an attack, server
implementers may want to consider logging such errors.) The purpose
of duplicating information from the request URL in this field is to
deal with the possibility that an intermediate proxy may alter the
client's Request-Line. This altered (but presumably semantically
equivalent) request would not result in the same digest as that
calculated by the client.

I'm using the Flurl library (v1.4), which is just a wrapper around HttpClient. However, HttpClientHandler is from .Net.

Why does it use the base URI and not the full URI? Is it a bug? How do I make it use the full URI?

I thought of adding another HttpMessageHandler into the pipeline and modifying the Authentication header with the full URI, but HttpClientHandler doesn't let you set an InnerHandler.

The full Uri should be: http://base/resource.cgi?my=1&params=2

But this is what appears in the request header:

Authorization: Digest username="user",realm="serve",nonce="5b911545:eaf4352d2113e5e4b1ca253bd70fd90a", uri="/base/resource.cgi",cnonce="bf7cf40f1289bc10bd07e8bf4784159c",nc=00000001,qop="auth",response="cf3c731ec93f7e5928f19f880f8325ab"

Which results in a 400 Bad Request response.

My Flurl Code:

HttpClient Factory

    /// <summary>
    /// Custom factory to generate HttpClient and handler to use digest authentication and a continuous stream
    /// </summary>
    private class DigestHttpFactory : Flurl.Http.Configuration.DefaultHttpClientFactory
    {
        private CredentialCache CredCache { get; set; }

        public DigestHttpFactory(string url, string username, string password) : base()
        {
            Url = url;

            CredCache = new CredentialCache
            {
                { new Uri(Url), "Digest", new NetworkCredential(username, password) }
            };
        }

        private string Url { get; set; }

        public override HttpClient CreateHttpClient(HttpMessageHandler handler)
        {
            var client = base.CreateHttpClient(handler);
            client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite); // To keep stream open indefinietly
            return client;
        }

        public override HttpMessageHandler CreateMessageHandler()
        {
            var handler = new HttpClientHandler
            {
                Credentials = CredCache.GetCredential(new Uri(Url), "Digest")
            };


            return handler;
        }
    }

Request code

public class MyConnection
{
    public string BaseUrl => "http://base/resource.cgi";

    public async Task ConnectAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        ConnectionCancellation = new CancellationTokenSource();

        var url = BaseUrl
            .SetQueryParam("my", 1)
            .SetQueryParam("params", 2)

        FlurlHttp.ConfigureClient(url, client =>
        {
            client.Configure(settings =>
            {
                settings.HttpClientFactory = new DigestHttpFactory(url, Username, Password);
            });
        });

        try
        {
            using (var getResponse = url.GetAsync(cancellationToken, HttpCompletionOption.ResponseHeadersRead))
            {
                var responseMessage = await getResponse;

                using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ConnectionCancellation.Token, cancellationToken))
                using (var stream = await responseMessage.Content.ReadAsStreamAsync())
                    await Process(stream, linkedCts.Token);
            }
        }
        catch (OperationCanceledException ex)
        {
            throw ex;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

Solution

  • It appears that this is indeed a bug with Microsoft's implementation of the RFC:

    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.

    That post details a work-around taken from this answer.