El encabezado de autorización se pierde al redirigir

A continuación se muestra el código que hace la autenticación, genera el encabezado Authorization y llama a la API.

Desafortunadamente, recibo un error 401 Unauthorized después de la solicitud GET en la API.

Sin embargo, cuando capturo el tráfico en Fiddler y lo reproduzco, la llamada a la API es exitosa y puedo ver el código de estado de 200 OK deseado.

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

Cuando ejecuto este código, el encabezado de Autorización se pierde.

Sin embargo, en Fiddler ese encabezado se pasa con éxito.

¿Alguna idea de lo que estoy haciendo mal?

La razón por la que está experimentando este comportamiento es que es por diseño .

La mayoría de los clientes HTTP (de forma predeterminada) eliminan los encabezados de autorización cuando se sigue un redireccionamiento.

Una de las razones es la seguridad. El cliente podría ser redirigido a un servidor de terceros que no sea de confianza, uno al que no le gustaría divulgar su token de autorización.

Lo que puede hacer es detectar que se ha producido la redirección y volver a enviar la solicitud directamente a la ubicación correcta.

Su API está devolviendo 401 Unauthorized para indicar que falta el encabezado de la autorización (o está incompleto). Asumiré que la misma API devuelve 403 Forbidden si la información de autorización está presente en la solicitud, pero es simplemente incorrecta (nombre de usuario / contraseña incorrectos).

Si este es el caso, puede detectar la combinación ‘redirigir / encabezado de autorización faltante’ y volver a enviar la solicitud.


Aquí está el código de la pregunta reescrita para hacer esto:

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

Tenga en cuenta que puede guardar el valor de finalRequestUri y usarlo para futuras solicitudes para evitar la solicitud adicional involucrada en el rebash. Sin embargo, como se trata de una redirección temporal, probablemente debas emitir la solicitud a la ubicación original cada vez.

Desactivaría el comportamiento de redireccionamiento automático y crearía un controlador de cliente que oculte el código que trata sobre el redireccionamiento temporal. La clase HttpClient permite instalar DelegatingHandler s desde donde puede modificar la solicitud de respuesta.

 public class TemporaryRedirectHandler : DelegatingHandler { protected override async Task 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 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 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 prop in request.Properties) { clone.Properties.Add(prop); } } private void CloneKeyValuePairs(HttpRequestMessage clone, HttpRequestMessage request) { foreach (KeyValuePair> header in request.Headers) { clone.Headers.TryAddWithoutValidation(header.Key, header.Value); } } } 

Instalarías el HttpClient así:

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

Tuve un problema similar, pero no del todo. En mi caso, también tuve el problema de redireccionamiento, pero la seguridad se implementa con OAuth, que también tiene el problema secundario, pero relacionado, de que los tokens a veces caducan.

Por ese motivo, me gustaría poder configurar un HttpClient para ir y actualizar automáticamente el token de OAuth cuando reciba una respuesta 401 Unauthorized , independientemente de si esto sucede debido a una redirección o caducidad de un token.

La solución publicada por Chris O’Neill muestra los pasos generales a seguir, pero quería incorporar ese comportamiento dentro de un objeto HttpClient , en lugar de tener que rodear todo nuestro código HTTP con un control imperativo. Tenemos una gran cantidad de código existente que utiliza un objeto HttpClient compartido, por lo que sería mucho más fácil refactorizar nuestro código si pudiera cambiar el comportamiento de ese objeto.

El siguiente parece que está funcionando. Hasta ahora solo lo prototipé, pero parece estar funcionando. Gran parte de nuestra base de código está en F #, por lo que el código 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 

Esta es una pequeña clase que se encarga de actualizar el encabezado Authorization si recibe una respuesta 401 Unauthorized . Se actualiza con la función refreshAuth inyectada, que tiene la unit -> Headers.AuthenticationHeaderValue type unit -> Headers.AuthenticationHeaderValue .

Como este sigue siendo un código de prototipo, hice que la llamada SendAsync interna SendAsync una llamada de locking, dejándola así como un ejercicio para que el lector la implemente correctamente utilizando un flujo de trabajo asíncrono.

Dada una función de actualización llamada refreshAuth , puede crear un nuevo objeto HttpClient como este:

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

La respuesta publicada por Chris O'Neill se encarga de verificar que la nueva URL aún se considere segura. Me salté esa consideración de seguridad aquí, pero debería considerar incluir un cheque similar antes de volver a intentar la solicitud.