web-dev-qa-db-ja.com

リダイレクト時に認証ヘッダーが失われます

以下は、認証を行い、Authorizationヘッダーを生成し、APIを呼び出すコードです。

残念ながら、401 Unauthorized AP​​IのGETリクエストに続くエラー。

ただし、Fiddlerでトラフィックをキャプチャして再生すると、APIの呼び出しは成功し、目的の200 OKステータスコード。

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

このコードを実行すると、Authorizationヘッダーが失われます。

ただし、Fiddlerでは、そのヘッダーは正常に渡されます。

私が間違っていることは何ですか?

20
Vadim

この動作が発生する理由は、設計によるであるためです。

ほとんどのHTTPクライアント(デフォルト)は、リダイレクトを追跡するときに認証ヘッダーを取り除きます。

1つの理由はセキュリティです。クライアントは、信頼されていない第三者サーバーにリダイレクトされる可能性があります。第三者サーバーは、認証トークンを公開したくないサーバーです。

できることは、リダイレクトが発生したことを検出し、正しい場所に直接リクエストを再発行することです。

APIが401 Unauthorizedを返し、認証ヘッダーが欠落している(または不完全である)ことを示しています。承認情報がリクエストに存在するが、単に間違っている(間違ったユーザー名/パスワード)場合、同じAPIが403 Forbiddenを返すと仮定します。

この場合、「リダイレクト/欠落した認証ヘッダー」の組み合わせを検出し、リクエストを再送信できます。


これを行うために書き直された質問のコードは次のとおりです。

[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;
}

finalRequestUriの値を保存し、今後の要求に使用して、再試行に伴う余分な要求を回避できることに注意してください。ただし、これは一時的なリダイレクトなので、おそらく毎回元の場所にリクエストを発行する必要があります。

48
Chris O'Neill

自動リダイレクト動作をオフにして、一時リダイレクトを処理するコードを隠すクライアントハンドラを作成します。 HttpClientクラスを使用すると、DelegatingHandlersをインストールして、そこから応答要求を変更できます。

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

次のようにHttpClientをインスタンス化します。

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

HttpClient client = new HttpClient(handler);
4
MvdD

同様の問題がありましたが、まったく同じではありませんでした。私の場合、リダイレクトの問題もありましたが、セキュリティはOAuthで実装されており、OAuthにはトークンが期限切れになるという二次的な問題があります。

そのため、HttpClientを設定して、401 Unauthorized応答を受信したときに、OAuthトークンを自動的に移動して更新するように設定したいこれがリダイレクトによるものか、トークンの有効期限によるものか。

Chris O'Neillによって投稿されたソリューションは、実行する一般的な手順を示していますが、すべてのHTTPコードを命令チェックで囲む必要はなく、HttpClientオブジェクト内にその動作を埋め込みたいと思いました。共有HttpClientオブジェクトを使用する既存のコードが多数あるため、そのオブジェクトの動作を変更できれば、コードをリファクタリングするのがはるかに簡単になります。

以下は動作しているようです。これまでプロトタイプを作成しただけですが、機能しているようです。コードベースの多くはF#にあるため、コードは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

これは、401 Unauthorized応答を受信した場合にAuthorizationヘッダーを更新する小さなクラスです。 unit -> Headers.AuthenticationHeaderValue型のrefreshAuth関数を挿入して更新します。

これはまだプロトタイプコードであるため、内側のSendAsync呼び出しをブロッキング呼び出しにして、非同期ワークフローを使用して適切に実装するための演習として読者に任せました。

refreshAuthと呼ばれる更新関数を指定すると、次のように新しいHttpClientオブジェクトを作成できます。

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

Chris O'Neillが投稿した回答は、新しいURLがまだ安全であると見なされることを確認するために注意を払います。ここではそのセキュリティに関する考慮事項をスキップしましたが、リクエストを再試行する前に同様のチェックを含めることを強く検討する必要があります。

0
Mark Seemann