web-dev-qa-db-fra.com

L'en-tête d'autorisation est perdu lors de la redirection

Vous trouverez ci-dessous le code qui effectue l'authentification, génère l'en-tête Authorization et appelle l'API.

Malheureusement, je reçois une erreur 401 Unauthorized après la demande GET sur l'API. 

Cependant, lorsque je capture le trafic dans Fiddler et que je le rejoue, l'appel à l'API aboutit et je peux voir le code d'état 200 OK souhaité.

[Test]
public void RedirectTest()
{
    HttpResponseMessage response;
    var client = new HttpClient();
    using (var authString = new StringContent(@"{username: ""theUser"", password: ""password""}", Encoding.UTF8, "application/json"))
    {
        response = client.PostAsync("http://Host/api/authenticate", authString).Result;
    }

    string result = response.Content.ReadAsStringAsync().Result;
    var authorization = JsonConvert.DeserializeObject<CustomAutorization>(result);
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authorization.Scheme, authorization.Token);
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.Host+json;version=1");

    response =
        client.GetAsync("http://Host/api/getSomething").Result;
    Assert.True(response.StatusCode == HttpStatusCode.OK);
}

Lorsque j'exécute ce code, l'en-tête d'autorisation est perdu.

Cependant, dans Fiddler, cet en-tête est passé avec succès.

Une idée de ce que je fais mal?

14
Vadim

La raison pour laquelle vous rencontrez ce problème est qu'il est par sa conception

La plupart des clients HTTP (par défaut) suppriment les en-têtes d'autorisation lors d'une redirection.

Une des raisons est la sécurité. Le client peut être redirigé vers un serveur tiers non approuvé, auquel vous ne souhaitez pas divulguer votre jeton d'autorisation.

Ce que vous pouvez faire est de détecter que la redirection a eu lieu et de relancer la demande directement au bon emplacement.

Votre API renvoie 401 Unauthorized pour indiquer que l'en-tête d'autorisation est manquant (ou incomplet). Je supposerai que la même API renvoie 403 Forbidden si les informations d'autorisation sont présentes dans la demande mais tout simplement incorrectes (nom d'utilisateur/mot de passe incorrect).

Si tel est le cas, vous pouvez détecter la combinaison "en-tête d'autorisation de redirection/manquante" et renvoyer la demande.


Voici le code de la question réécrite pour le faire:

[Test]
public void RedirectTest()
{
    // These lines are not relevant to the problem, but are included for completeness.
    HttpResponseMessage response;
    var client = new HttpClient();
    using (var authString = new StringContent(@"{username: ""theUser"", password: ""password""}", Encoding.UTF8, "application/json"))
    {
        response = client.PostAsync("http://Host/api/authenticate", authString).Result;
    }

    string result = response.Content.ReadAsStringAsync().Result;
    var authorization = JsonConvert.DeserializeObject<CustomAutorization>(result);

    // Relevant from this point on.
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authorization.Scheme, authorization.Token);
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.Host+json;version=1");

    var requestUri = new Uri("http://Host/api/getSomething");
    response = client.GetAsync(requestUri).Result;

    if (response.StatusCode == HttpStatusCode.Unauthorized)
    {
        // Authorization header has been set, but the server reports that it is missing.
        // It was probably stripped out due to a redirect.

        var finalRequestUri = response.RequestMessage.RequestUri; // contains the final location after following the redirect.

        if (finalRequestUri != requestUri) // detect that a redirect actually did occur.
        {
            if (IsHostTrusted(finalRequestUri)) // check that we can trust the Host we were redirected to.
            {
               response = client.GetAsync(finalRequestUri).Result; // Reissue the request. The DefaultRequestHeaders configured on the client will be used, so we don't have to set them again.
            }
        }
    }

    Assert.True(response.StatusCode == HttpStatusCode.OK);
}


private bool IsHostTrusted(Uri uri)
{
    // Do whatever checks you need to do here
    // to make sure that the Host
    // is trusted and you are happy to send it
    // your authorization token.

    if (uri.Host == "Host")
    {
        return true;
    }

    return false;
}

Notez que vous pouvez enregistrer la valeur finalRequestUri et l'utiliser pour les futures requêtes afin d'éviter la requête supplémentaire impliquée dans la nouvelle tentative. Cependant, comme il s’agit d’une redirection temporaire, vous devriez probablement envoyer la demande à l’emplacement d’origine à chaque fois.

42
Chris O'Neill

Je voudrais désactiver le comportement de redirection automatique et créer un gestionnaire de client qui cache le code traitant de la redirection temporaire. La classe HttpClient vous permet d'installer DelegatingHandlers à partir duquel vous pouvez modifier la demande de réponse.

public class TemporaryRedirectHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);
        if (response.StatusCode == HttpStatusCode.TemporaryRedirect)
        {
            var location = response.Headers.Location;
            if (location == null)
            {
                return response;
            }

            using (var clone = await CloneRequest(request, location))
            {
                response = await base.SendAsync(clone, cancellationToken);
            }
        }
        return response;
    }


    private async Task<HttpRequestMessage> CloneRequest(HttpRequestMessage request, Uri location)
    {
        var clone = new HttpRequestMessage(request.Method, location);

        if (request.Content != null)
        {
            clone.Content = await CloneContent(request);
            if (request.Content.Headers != null)
            {
                CloneHeaders(clone, request);
            }
        }

        clone.Version = request.Version;
        CloneProperties(clone, request);
        CloneKeyValuePairs(clone, request);
        return clone;
    }

    private async Task<StreamContent> CloneContent(HttpRequestMessage request)
    {
        var memstrm = new MemoryStream();
        await request.Content.CopyToAsync(memstrm).ConfigureAwait(false);
        memstrm.Position = 0;
        return new StreamContent(memstrm);
    }

    private void CloneHeaders(HttpRequestMessage clone, HttpRequestMessage request)
    {
        foreach (var header in request.Content.Headers)
        {
            clone.Content.Headers.Add(header.Key, header.Value);
        }
    }

    private void CloneProperties(HttpRequestMessage clone, HttpRequestMessage request)
    {
        foreach (KeyValuePair<string, object> prop in request.Properties)
        {
            clone.Properties.Add(prop);
        }
    }

    private void CloneKeyValuePairs(HttpRequestMessage clone, HttpRequestMessage request)
    {
        foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
        {
            clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
        }
    }
}

Vous pouvez instancier le HttpClient comme ceci:

var handler = new TemporaryRedirectHandler()
{
    InnerHandler = new HttpClientHandler()
    {
        AllowAutoRedirect = false
    }
};

HttpClient client = new HttpClient(handler);
2
MvdD

J'ai eu un problème similaire, mais pas tout à fait le même. Dans mon cas, le problème de redirection était également présent, mais la sécurité est implémentée avec OAuth, qui présente également le problème secondaire, mais lié, selon lequel les jetons expirent parfois.

Pour cette raison, j'aimerais pouvoir configurer HttpClient pour qu'il actualise automatiquement le jeton OAuth lorsqu'il reçoit une réponse 401 Unauthorized, que cela soit dû à une redirection ou à une expiration du jeton.

La solution publiée par Chris O'Neill montre les étapes générales à suivre, mais je voulais intégrer ce comportement à l'intérieur d'un objet HttpClient, au lieu de devoir entourer tout notre code HTTP d'un contrôle impératif. Nous avons beaucoup de code existant qui utilise un objet HttpClient partagé, il serait donc beaucoup plus facile de refactoriser notre code si je pouvais changer le comportement de cet objet.

Ce qui suit semble que cela fonctionne. Je l'ai seulement prototypé jusqu'à présent, mais cela semble fonctionner. Une grande partie de notre base de code est en F #, donc le code est en F #:

open System.Net
open System.Net.Http

type TokenRefresher (refreshAuth, inner) =
    inherit MessageProcessingHandler (inner)

    override __.ProcessRequest (request, _) = request

    override __.ProcessResponse (response, cancellationToken) =
        if response.StatusCode <> HttpStatusCode.Unauthorized
        then response
        else
            response.RequestMessage.Headers.Authorization <- refreshAuth ()
            inner.SendAsync(response.RequestMessage, cancellationToken).Result

C'est une petite classe qui s'occupe d'actualiser l'en-tête Authorization si elle reçoit une réponse 401 Unauthorized. Il est actualisé à l'aide d'une fonction refreshAuth injectée, de type unit -> Headers.AuthenticationHeaderValue.

Puisqu'il s'agit toujours d'un code prototype, j'ai fait l'appel SendAsync intérieur d'un appel bloquant, le laissant ainsi au lecteur comme un exercice pour le mettre en œuvre correctement à l'aide d'un flux de travail async.

Avec une fonction d'actualisation appelée refreshAuth, vous pouvez créer un nouvel objet HttpClient comme ceci:

let client = new HttpClient(new TokenRefresher(refreshAuth, new HttpClientHandler ()))

La réponse publiée par Chris O'Neill prend soin de vérifier que la nouvelle URL est toujours considérée comme étant sûre. J'ai ignoré cette considération de sécurité ici, mais vous devriez fortement envisager d'inclure une vérification similaire avant de réessayer la demande.

0
Mark Seemann