6

My code is making an HTTP GET to a web service URL that requires basic authentication.

I've implemented this using an HttpClient with an HttpClientHandler that has the Credentials property defined.

This all works perfectly.. Except for one of my use-cases where I'm making the authenticated GET to: http://somedomain.com which redirects to http://www.somedomain.com.

It seems that the HttpClientHandler clears the authentication header during the redirect. How can I prevent this? I want the credentials to be sent regardless of redirects.

This is my code:

// prepare the request
var request = new HttpRequestMessage(method, url);
using (var handler = new HttpClientHandler { Credentials = new NetworkCredential(username, password) , PreAuthenticate = true })
using (var client = new HttpClient(handler))
{
    // send the request
    var response = await client.SendAsync(request);

Note: this is a related question: Keeping HTTP Basic Authentification alive while being redirected But since I'm using different classes for making the request, there might be a better, more specific solution

1
  • side note, I think the designed behavior makes no sense in this case. I set the credentials as part of my client, not per a specific URI (the request). Since the same client can perform multiple requests and the authorization will be sent regardless of their URIs, this is quite silly Commented Oct 21, 2013 at 11:59

3 Answers 3

5

The default HttpClientHandler uses the same HttpWebRequest infrastructure under the covers. Instead of assigning a NetworkCredential to the Credentials property, create a CredentialCache and assign that.

This is what I use in place of the AutoRedirect and with a little async/await fairy dust it would probably be a whole lot prettier and more reliable.

 public class GlobalRedirectHandler : DelegatingHandler {

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
        var tcs = new TaskCompletionSource<HttpResponseMessage>();

        base.SendAsync(request, cancellationToken)
            .ContinueWith(t => {
                HttpResponseMessage response;
                try {
                    response = t.Result;
                }
                catch (Exception e) {
                    response = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);
                    response.ReasonPhrase = e.Message;
                }
                if (response.StatusCode == HttpStatusCode.MovedPermanently
                    || response.StatusCode == HttpStatusCode.Moved
                    || response.StatusCode == HttpStatusCode.Redirect
                    || response.StatusCode == HttpStatusCode.Found
                    || response.StatusCode == HttpStatusCode.SeeOther
                    || response.StatusCode == HttpStatusCode.RedirectKeepVerb
                    || response.StatusCode == HttpStatusCode.TemporaryRedirect

                    || (int)response.StatusCode == 308) 
                {

                    var newRequest = CopyRequest(response.RequestMessage);

                    if (response.StatusCode == HttpStatusCode.Redirect 
                        || response.StatusCode == HttpStatusCode.Found
                        || response.StatusCode == HttpStatusCode.SeeOther)
                    {
                        newRequest.Content = null;
                        newRequest.Method = HttpMethod.Get;

                    }
                    newRequest.RequestUri = response.Headers.Location;

                    base.SendAsync(newRequest, cancellationToken)
                        .ContinueWith(t2 => tcs.SetResult(t2.Result));
                }
                else {
                    tcs.SetResult(response);
                }
            });

        return tcs.Task;
    }

    private static HttpRequestMessage CopyRequest(HttpRequestMessage oldRequest) {
        var newrequest = new HttpRequestMessage(oldRequest.Method, oldRequest.RequestUri);

        foreach (var header in oldRequest.Headers) {
            newrequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
        }
        foreach (var property in oldRequest.Properties) {
            newrequest.Properties.Add(property);
        }
        if (oldRequest.Content != null) newrequest.Content = new StreamContent(oldRequest.Content.ReadAsStreamAsync().Result);
        return newrequest;
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

The problem with CredentialCache is that I need to know prematurely the URI post redirection - which I don't. The API endpoint is configurable by users and these users might forget to give the www, or decide to buy a new domain some day..
@talkol Ok. Then turn off the autoredirect and write your own message handler to do the redirect. It's fairly easy to do. Just be aware of the security concerns of sending those credentials to any arbitrary site.
yeah thanks, that's what I ended up doing. with async-await fairy dust of course ;)
@DarrelMiller See my answer, I improved your code a bit and also included a simple check to avoid sending the credentials to different hosts.
0

I used @DarrelMiller 's solution and it works. However, I did some improvements

I refactored the code so everything is in CopyRequest which now takes the response as an argument.

var newRequest = CopyRequest(response);

base.SendAsync(newRequest, cancellationToken)
    .ContinueWith(t2 => tcs.SetResult(t2.Result));

This is the CopyRequest method with my improvements

  • Instead of creating a new StreamContent and set it to null for Redirect / Found / SeeOther the content is only set if necesarry.
  • RequestUri is only set if Location is set and takes into account that it may not be a relative uri.
  • Most important: I check for the new Uri and if the host does not match I do not copy the autorization header, to prevent leaking your credentials to an external host.
private static HttpRequestMessage CopyRequest(HttpResponseMessage response)
{
    var oldRequest = response.RequestMessage;

    var newRequest = new HttpRequestMessage(oldRequest.Method, oldRequest.RequestUri);

    if (response.Headers.Location != null)
    {
        if (response.Headers.Location.IsAbsoluteUri)
        {
            newRequest.RequestUri = response.Headers.Location;
        }
        else
        {
            newRequest.RequestUri = new Uri(newRequest.RequestUri, response.Headers.Location);
        }
    }

    foreach (var header in oldRequest.Headers)
    {
        if (header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase) && !(oldRequest.RequestUri.Host.Equals(newRequest.RequestUri.Host)))
        {
            //do not leak Authorization Header to other hosts
            continue;
        }
        newRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
    }

    foreach (var property in oldRequest.Properties)
    {
        newRequest.Properties.Add(property);
    }

    if (response.StatusCode == HttpStatusCode.Redirect
        || response.StatusCode == HttpStatusCode.Found
        || response.StatusCode == HttpStatusCode.SeeOther)
    {
        newRequest.Content = null;
        newRequest.Method = HttpMethod.Get;
    }
    else if (oldRequest.Content != null)
    {
        newRequest.Content = new StreamContent(oldRequest.Content.ReadAsStreamAsync().Result);
    }

    return newRequest;
}

2 Comments

Oh, thanks. I didn't realize my original solution didn't have the host check. I should make sure our Graph middleware includes your optimizations github.com/microsoftgraph/msgraph-sdk-dotnet-core/blob/dev/src/…
Thanks for pointing me to the code. That solved another problem I had (the CloneAsync extension method in particular): // HttpClient doesn't rewind streams and we have to explicitly do so.. Also I looks like the code already has the propper host check.
0

Based on the answers by Darrel Miller (https://stackoverflow.com/a/19493338/227779) and Jürgen Steinblock (https://stackoverflow.com/a/60277550), I've constructed the following handler. My changes:

  • Updated to be in sync with modern C# (async/await)
  • Complete example (Jürgen's example only included the CopyRequest method + some details on how to change the call site)
  • List of status codes slightly different - the numeric values are the same, but Darrel's list contained a few duplicates
  • Supports redirects in multiple levels (i.e. 302 Found -> 302 Found -> 200 OK), which was needed for our use case. A maximum of 10 levels of redirects is set, to make sure we break if we encounter redirect loops.

HttpClient creation

Use like this:

var innerHandler = new HttpClientHandler {
    AllowAutoRedirect = false
};

var withAuthorizationHandler = new SameHostAllowRedirectWithAuthorizationHandler {
    InnerHandler = innerHandler
};

var client = new HttpClient(withAuthorizationHandler);

SameHostAllowRedirectWithAuthorizationHandler implementation

// Based on examples from https://stackoverflow.com/a/19493338/227779 (posted by Darrel Miller
// (MIT-licensed)) and https://stackoverflow.com/a/60277550 (posted by Jürgen Steinblock (CC
// BY-SA 4.0))
private class SameHostAllowRedirectWithAuthorizationHandler : DelegatingHandler {
    private const int MaxRedirects = 10;

    private readonly HttpStatusCode[] RedirectStatusCodes = {
        HttpStatusCode.MovedPermanently,
        HttpStatusCode.Found,
        HttpStatusCode.SeeOther,
        // 304 Not Modified is not a redirect
        // 305 Use Proxy ignored
        // 306 Switch Proxy no longer used
        HttpStatusCode.TemporaryRedirect,
        HttpStatusCode.PermanentRedirect
    };
    
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
        var response = await base.SendAsync(request, cancellationToken);
    
        for (int i = 0; i < MaxRedirects; i++) {
            if (RedirectStatusCodes.Contains(response.StatusCode)) {
                var newRequest = CopyRequest(response);
                response = await base.SendAsync(newRequest, cancellationToken);
            }
            else {
                return response;
            }
        }
    
        throw new Exception($"Maximum number of redirects ({MaxRedirects}) reached. Is there a redirect loop somewhere?");
    }
    
    private static HttpRequestMessage CopyRequest(HttpResponseMessage response) {
        var oldRequest = response.RequestMessage!;
    
        var newRequest = new HttpRequestMessage(oldRequest.Method, oldRequest.RequestUri);
    
        if (response.Headers.Location != null) {
            if (response.Headers.Location.IsAbsoluteUri) {
                newRequest.RequestUri = response.Headers.Location;
            }
            else {
                newRequest.RequestUri = new Uri(newRequest.RequestUri, response.Headers.Location);
            }
        }
    
        foreach (var header in oldRequest.Headers) {
            if (header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase) && !(oldRequest.RequestUri.Host.Equals(newRequest.RequestUri.Host))) {
                // Do not leak Authorization Header to other hosts
                continue;
            }
    
            newRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
        }
    
        foreach (var property in oldRequest.Properties) {
            newRequest.Properties.Add(property);
        }
    
        if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.SeeOther) {
            newRequest.Content = null;
            newRequest.Method = HttpMethod.Get;
        }
        else if (oldRequest.Content != null) {
            newRequest.Content = new StreamContent(oldRequest.Content.ReadAsStreamAsync().Result);
        }
    
        return newRequest;
    }
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.