Securizando tus servicios WebApi usando OWIN

Hace algún tiempo escribí un post acerca de mecanismos para hacer tus servicios WebApi seguros. En este post mencionaba tres mecanismos para que tus servicios web solo sean accesibles a través de usuarios autenticados:

  • Atributo Authorize custom: no recomendado para autenticación (es para autorización).
  • Message Handler (DelegatingHandler): Mecanismo recomendado dentro de WebApi para autenticación.
  • HttpModule: Solución a nivel de IIS.

En el post comentaba que si tu aplicación depende de IIS puedes usar un HttpModule, pero que si quieres evitar tener una dependencia con él (ya que WebApi admite escenarios selfhost) debes usar un DelegatingHandler.

Pero este post está escrito antes de que OWIN fuese una realidad y con la aparición de OWIN existe otro mecanismo para autenticar nuestros servicios WebApi: usar un middleware OWIN.

Usar un middleware OWIN une las dos ventajas de los Message Handlers y los HttpModules:

  1. Al igual que un HttpModule funciona antes que WebApi por lo que autentica no solo llamadas a WebApi si no también llamadas a otros componentes (p. ej. MVC) que podamos tener.
  2. Al igual que un DelegatingHandler no está ligado a IIS en tanto que OWIN no está ligado a ningún servidor web en concreto.

El objetivo de este post es ver como crear un middleware OWIN para autenticar nuestros servicios WebApi, aunque si usáramos algún otro componente compatible con OWN (p. ej. NancyFx) nos serviría para autenticar las llamadas a ese otro componente también.

Vamos a empezar creando una aplicación ASP.NET Empty con WebApi agregado:

image

Luego agregaremos los paquetes Katana (para los despistados: Katana es una implementación de paquetes OWIN desarrollada por MS). En concreto instalaremos con NuGet el paquete Microsoft.Owin.Host.SystemWeb y este instalará ya todas las dependencias.

Creando el middleware OWIN

Un middleware OWIN es algo extraordinariamente simple. Es tan simple que no es ni una clase. Es tan solo un delegado. En su forma más básica un middleware OWIN es un Func<IDictionary<string, object>, Task>.

Aunque podríamos registrar directamente un delegado con esa firma, el sistema nos permite también registrar una clase, siempre y cuando tenga la siguiente estructura:

  1. using AppFunc = Func<IDictionary<string, object>, Task>;
  2. public class BasicAuthMiddleware
  3. {
  4.     private readonly AppFunc _next;
  5.     public BasicAuthMiddleware(AppFunc next)
  6.     {
  7.  
  8.     }
  9.  
  10.     public async Task Invoke(IDictionary<string, object> environment)
  11.     {
  12.  
  13.     }
  14. }

Este es el esqueleto básico para crear un middleware OWIN. Ahora hagamos que este middleware haga algo. Lo que hará será validar que existe autenticación básica (es decir que la cabecera HTTP Authorization llega y tiene el valor “Basic XXXX” donde XXXX es la cadena Base64 resultado de concatenar un login y un password separados por dos puntos (:).

Para ser súper genérico y no atarse a nadie, OWIN no define clases para acceder a las cabeceras HTTP, al cuerpo HTTP ni a nada. En su lugar tan solo se define el entorno: un diccionario de claves (cadena) valores (objeto). Pocas cosas hay más genéricas que eso.

¿Y qué valores tienen esas claves? Pues hay algunas que define la propia especificación, pero luego tu módulo OWIN podría añadir las que quisiera… no dejan de ser un mecanismo de comunicación entre middlewares.

Veamos ahora el código del método

El código del constructor de la clase es trivial (simplemente nos guardamos en _next el valor del parámetro que recibimos).

Veamos el código del método Invoke:

  1. public async Task Invoke(IDictionary<string, object> environment)
  2. {
  3.     var auth = GetAuthHeader(environment);
  4.     if (!string.IsNullOrEmpty(auth))
  5.     {
  6.         if (auth.StartsWith("Basic "))
  7.         {
  8.             auth = auth.Substring("Basic ".Length);
  9.             var values = DecodeAuthValue(auth);
  10.             // Validaramos login (values.Item1) y pwd (values.Item2) aqu.
  11.             // En este ejemplo suponemos que cualquier combinacin es OK
  12.             PutAuthenticationInfo(environment, values.Item1);
  13.         }
  14.     }
  15.     await _next.Invoke(environment);
  16. }

Simplemente recogemos el valor del header Authorization y si existe y tiene el formato esperado (Basic <CadenaBase64>) lo decodificamos y colocamos la identidad del usuario en el contexto de OWIN.

El método GetAuthHeader obtiene el valor de la cabecera Authorization:

  1. private string GetAuthHeader(IDictionary<string, object> environment)
  2. {
  3.     var headers = environment["owin.RequestHeaders"] as IDictionary<string, string[]>;
  4.     if (headers != null && headers.ContainsKey("Authorization"))
  5.     {
  6.         return headers["Authorization"].FirstOrDefault();
  7.     }
  8.     return null;
  9. }

Los headers se almacenan dentro del contexto en la clave owin.RequestHeaders y el valor de dicha clave es un diccionario con todos los headers. Dado que un header puede tener varios valores el diccionario es de cadena (nombre del header) a valor (array de cadenas). En este caso devolvemos tan solo el primer que haya.

El método DecodeAuthValue simplemente decodifica la cadena Base64 y devuelve una tupla con el login y el password que estaban en la cabecera:

  1. private Tuple<string, string> DecodeAuthValue(string auth)
  2. {
  3.     var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(auth));
  4.     var tokens = decoded.Split(':');
  5.     return new Tuple<string, string>(tokens[0], tokens.Length > 1 ? tokens[1] : null);
  6. }

Y finalmente el método PutAuthenticationInfo es el que crea el IPrincipal y lo coloca dentro del contexto de OWIN. Ya no debemos colocarlo en HttpContext.Current ni Thread.CurrentPrincipal. Debemos colocarlo dentro de la clave “server.User” del entorno OWIN:

  1. private void PutAuthenticationInfo(IDictionary<string, object> environment, string user)
  2. {
  3.     var claim = new Claim(ClaimTypes.Name, user);
  4.     var identity = new ClaimsIdentity(new[] {claim}, "Basic");
  5.     environment["server.User"] = new ClaimsPrincipal(identity);
  6. }

En este caso simplemente creamos un ClaimsPrincipal (con el nombre del usuario) y lo colocamos dentro del contexto.

El último punto es  configurar OWIN para que use nuestro middleware. Añade una clase, llámala Startup y coloca el siguiente código:

  1. [assembly: OwinStartup(typeof(OwinSecureDemo.Startup))]
  2.  
  3. namespace OwinSecureDemo
  4. {
  5.     public class Startup
  6.     {
  7.         public void Configuration(IAppBuilder app)
  8.         {
  9.             app.Use(typeof (BasicAuthMiddleware));
  10.         }
  11.     }
  12. }

Finalmente para probar creamos un controlador WebApi y lo decoramos con [Authorize]:

  1. [Authorize]
  2. public class BeersController : ApiController
  3. {
  4.     public IEnumerable<string> Get()
  5.     {
  6.         return new[] {"Estrella", "Voll Damm"};
  7.     }
  8. }

¡Y listos! Ya puedes probarlo pasando un valor válido a la cabecera Authorization y ver que todo funciona:

curl "http://localhost:23850/api/Beers" -H "Accept: application/json" -H "Authorization: Basic ZWl4aW1lbmlzOnB3ZA=="

Luego quitas la cabecera Authorization de la llamada y deberías recibir un error de usuario no autenticado.

Hemos visto lo sencillo que es usar un middleware OWIN para autenticación y además lo hemos implementado desde cero (sin usar ninguna clase dependiente de Katana tal como OwinContext u OwinRequest). Katana ofrece clases wrappers sobre el contexto OWIN para no tener que andar metiendo claves “a mano” en el diccionario que es el contexto de OWIN, pero eso ya sería motivo de otro post.

Un saludo!

Securizar tu WebApi con Azure Mobile Services

El escenario es el siguiente: tenemos un conjunto de servicios WebApi que no tienen porque estar desplegados en Azure pero queremos que esos servicios solo sean accesibles para usuarios que se hayan autenticado previamente a través de un proveedor externo (p. ej. Twitter) usando la infrastructura de Azure Mobile Services.

Creación y configuración de Mobile Services

Esa es la parte fácil… Una vez hemos creado nuestro mobile service debemos irnos a la pestaña Identity y colocar los valores que nos pide según el proveedor de autenticación externo que queramos usar. En este ejemplo usaremos twitter así que nos pide el Api Key y el Api Secret:

image

Estos valores nos lo da twitter cuando creamos una aplicación en apps.twitter.com:

image

Otro aspecto a tener en cuenta es que twitter nos pide la URL de callback, esa debe ser la URL de nuestro mobile service añadiendo el sufijo /signin-twitter.

Pero bueno… todos esos detalles están explicados fenomenalmente en la propia ayuda de Azure.

Autenticarse usando twitter a través de Mobile Services (WAMS)

El cliente de Azure Mobile Services realiza un trabajo genial a la hora de gestionar todo el flujo oAuth para autenticar a usuarios usando cualquiera de los proveedores que soporta (Facebook, twitter, Google, una cuenta de Live ID). Si creas una aplicación Windows 8.1 o bien Windows Phone, autenticarse usando un proveedor externo (twitter) a través de Mobile Services es trivial:

  1. public async void PerformAuth()
  2. {
  3.     MobileServiceClient client = new MobileServiceClient("https://beerlover.azure-mobile.net/");
  4.     var user = await client.LoginAsync(MobileServiceAuthenticationProvider.Twitter);
  5.     JwtToken = user.MobileServiceAuthenticationToken;
  6. }

Este código se encarga de gestionar todo el flujo OAuth y mostrar la UI correspondiente para que el usuario pueda introducir su login y password de twitter. Al final guarda en una propiedad JwtToken el token de autenticación retornado por WAMS.

En este punto finaliza nuestra interacción con mobile services: lo hemos usado para que el usuario se pudiese autenticar de forma fácil con un proveedor externo. Y al final obtenemos un token. Ese token no es de twitter, ese token es de WAMS y es un token JWT.

Json Web Token – JWT

JWT es un formato que define datos que puede incorporar un token que realmente es un objeto JSON. Es un formato que se está usando bastante ahora y tienes su especificación aquí.

Si ejecuto una aplicación que tenga el código anterior y me autentico usando twitter puedo ver como es el token JWT que me devuelve Mobile Services:

image

Bueno… es una ristra de carácteres bastante larga, pero básicamente se trata del objeto JSON codificado en Base64.

Para ver el contenido, puedes copiar el token y usar jwt.io para descodificar el token:

image

Podemos ver que hay tres colores en la imagen anterior y cada uno se corresponde a una parte. El primer color (verde) es un JSON con el siguiente formato:

  1. {
  2.   "typ": "JWT",
  3.   "alg": "HS256"
  4. }

Esto se conoce como JWT Envelope y nos indica el formato del token que viene a continuación (JWT) y el algoritmo usado para la firma digital (HS256).

La siguiente parte del token (azul) es realmente el JSON con los datos:

  1. {
  2.   "iss": "urn:microsoft:windows-azure:zumo",
  3.   "aud": "urn:microsoft:windows-azure:zumo",
  4.   "nbf": 1418892674,
  5.   "exp": 1421484674,
  6.   "urn:microsoft:credentials": "{\"accessToken\":\"84274067-6C7zM6rnbL5VAIF8ARIZXWg6XTZ49x67klxRTAyIU\",\"accessTokenSecret\":\"fGDwXuJg7i8rJqwJFTki5EVbEiLvgx3MlCvunOaNOm81X\"}",
  7.   "uid": "Twitter:84274067",
  8.   "ver": "2"
  9. }

Realmente cada uno de esos campos es un claim (JWT está basado en claims).

De esos claims nos interesan realmente dos. El claim iss que contiene el issuer o quien ha generado el token y el claim aud que contiene la audience que indica para quien va dirigido el token. En este caso iss nos indica que el token ha sido generado por Mobile Services y el token aud nos indica que el token es para consumo de Mobile Services. Podemos ver más claims como uid donde hay un ID de usuario.

La última parte (en rojo en la imágen) es la firma digital del token. En este caso el Envelope nos indica que la firma digital es HS256 (HMAC usando SHA256) y eso nos sirve para comprobar que el token es válido.

Securizando nuestros servicios WebApi

La idea para securizar nuestros servicios WebApi es muy simple: A cada petición de nuestros servicios miraremos si se nos pasa el token JWT en la cabcera HTTP Authentication. Si recibimos un token JWT lo parsearemos y comprobaremos la firma digital (podríamos comprobar además todos los claims que queramos). Si la firma digital es válida, el token es válido y la petición se considera que proviene de un usuario de confianza.

Para validar el token JWT vamos a usar un DelegatingHandler. Ya expliqué en un post anterior sobre como securizar servicios WebApi lo que era un DelegatingHandler.

Para la validación del token JWT nos vamos a ayudar del paquete System.IdentityModel.Tokens.Jwt así que lo primero es agregarlo a la solución. Eso sí agrega la versión 3.0.0.0 mediante el comando:

Install-Package System.IdentityModel.Tokens.Jwt -Version 3.0.0.0

Esa versión es la misma que usa internamente Mobile Services para generar el token JWT.

Así lo primero es crearte una clase (yo la he llamado JwtDelegatingHandler) que derive de DelegatingHandler y redefinir el método SendAsync:

  1. protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  2. {
  3.     if (request.Method != HttpMethod.Options)
  4.     {
  5.         var tokenstr = RetrieveToken(request);
  6.         if (tokenstr != null)
  7.         {
  8.             var handler = new JwtSecurityTokenHandler();
  9.             if (handler.CanReadToken(tokenstr))
  10.             {
  11.                 var token = handler.ReadToken(tokenstr);
  12.                 var secret = GetSigningKey(MasterKey);
  13.                 var validationParams = new TokenValidationParameters()
  14.                 {
  15.                     SigningToken = new BinarySecretSecurityToken(secret),
  16.                     AllowedAudience = "urn:microsoft:windows-azure:zumo",
  17.                     ValidIssuer = "urn:microsoft:windows-azure:zumo"
  18.                 };
  19.                 var principal = handler.ValidateToken(tokenstr, validationParams);
  20.                 Thread.CurrentPrincipal = principal;
  21.                 if (HttpContext.Current != null)
  22.                 {
  23.                     HttpContext.Current.User = principal;
  24.                 }
  25.             }
  26.         }
  27.     }
  28.     return base.SendAsync(request, cancellationToken);
  29. }

El método usa la clase JwtSecurityTokenHandler para mirar si el token JWT es válido sintácticamente y luego llama al método ValidateToken que devuelve un ClaimsPrincipal si el token es correcto. Dicho método valida la firma digital y también comprueba que el issuer y el audence del token sean correctos. Es por ello que los establecemos a los valores que coloca WAMS.

Finalmente si no salta ninguna excepción es que el token es correcto por lo que establecemos el ClaimsPrincipal devuelto como Principal del Thread actual de forma que el atributo [Authorize] entenderá que la petición está autenticada.

El método RetrieveToken obtiene el token JWT de la cabecera:

  1. private static string RetrieveToken(HttpRequestMessage request)
  2.  {
  3.      string token = null;
  4.      IEnumerable<string> authzHeadersEnum;
  5.      bool hasHeader = request.Headers.TryGetValues("Authorization", out authzHeadersEnum);
  6.      if (!hasHeader)
  7.      {
  8.          return null;
  9.      }
  10.  
  11.      var authzHeaders = authzHeadersEnum.ToList();
  12.      if (authzHeaders.Count > 1)
  13.      {
  14.          return null;
  15.      }
  16.  
  17.      var bearerToken = authzHeaders[0];
  18.      token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
  19.      return token;
  20.  }

Y nos queda el método más importante el método GetSigninKey. Dicho método obtiene la clave que usó WAMS para firmar el mensaje y es la misma que debemos usar para comprobar la firma:

  1. internal static byte[] GetSigningKey(string secretKey)
  2. {
  3.     var bytes = new UTF8Encoding(true, true).GetBytes(secretKey);
  4.     using (SHA256Managed managed = new SHA256Managed())
  5.     {
  6.         return managed.ComputeHash(bytes);
  7.     }
  8. }

El parámetro secretKey es la “Master Key” de WAMS que puedes ver en el portal de Azure en la opción de “Manage Keys”:

image

Nota: Si buscas por Internet verás que en muchos sitios agregan la cadena JWTSig a la Master Key. No lo hagas. No sé si en versiones anteriores de WAMS era necesario, pero ahora no lo es.

Con esto ya puedes usar [Authorize] para proteger tus servicios WebApi y que solo sean válidos para aquellas peticiones que tengan un token JWT que proviene de Windows Azure Mobile Services.

Usando System.IdentityModel.Tokens.Jwt 4.0.1

Hemos visto el código necesario para validar el token JWT de WAMS usando la versión 3.0.0.0 de System.IdentityModel.Tokens.Jwt que es la que usa internamente WAMS. Pero la última versión de este paquete (y la que os instalará NuGet si no indicáis versión) es, a la hora de escribir este post, la 4.0.1.

El código no es compatible ya que hay cambios en la clase JwtSecurityTokenHandler. Si usáis la versión 4.0.1 debéis usar el siguiente código en el DelegatingHandler:

  1. var validationParams = new TokenValidationParameters()
  2. {
  3.     IssuerSigningToken = new BinarySecretSecurityToken(secret),
  4.     ValidAudience = "urn:microsoft:windows-azure:zumo",
  5.     ValidIssuer = "urn:microsoft:windows-azure:zumo"
  6. };
  7. SecurityToken outToken;
  8. var principal = handler.ValidateToken(tokenstr, validationParams, out outToken);

Este código es equivalente al anterior, podéis ver que los cambios básicos son nombres de propiedades y la firma de ValidateToken.

Usando Middleware OWIN

Comprobar que System.IdentityModel.Tokens.Jwt en su versión 4.0.1 era compatible con las firmas generadas por WAMS para los tokens JWT supone que podemos usar el middleware OWIN para validar el token JWT y olvidarnos del DelegatingHandler. Si la versión 4.0.1 no hubiese sido compatible (y algo de eso he leído en Internet, al menos en la versión 4.0.0) eso no sería posible ya que el middleware de OWIN depende de dicha versión (realmente la 4.0.0) para la validación de los tokens JWT.

Vale, para agregar el middleware OWIN basta con instalar el paquete Microsoft.Owin.Security.Jwt:

install-package Microsoft.Owin.Security.Jwt

Eso nos instalará las dependencias OWIN que tenemos, pero si no teníamos nada de OWIN instalada deberemos agregar el hosting de OWIN manualmente. Si después de este install-package no tenemos el paquete Microsoft.Owin.Host.SystemWeb deberemos instalarlo. Este paquete es el responsable de integrar el pipeline de OWIN y que se ejecute la clase de Owin Startup. El siguiente paso será crear una clase Startup:

  1. [assembly: OwinStartup(typeof(Beerlover.Server.Startup))]
  2.  
  3. namespace Beerlover.Server
  4. {
  5.     public class Startup
  6.     {
  7.         public void Configuration(IAppBuilder app)
  8.         {
  9.  
  10.             var issuer = "urn:microsoft:windows-azure:zumo";
  11.             var audience = "urn:microsoft:windows-azure:zumo";
  12.             var secret = WebConfigurationManager.AppSettings["ClientSecret"];
  13.  
  14.             var signkey = GetSigningKey(secret);
  15.             app.UseJwtBearerAuthentication(
  16.                 new JwtBearerAuthenticationOptions
  17.                 {
  18.                     AuthenticationMode = AuthenticationMode.Active,
  19.                     AllowedAudiences = new[] { audience },
  20.                     IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
  21.                     {
  22.                         new SymmetricKeyIssuerSecurityTokenProvider(issuer, signkey)
  23.                     },
  24.  
  25.                 });
  26.         }
  27.  
  28.         private byte[] GetSigningKey(string secret)
  29.         {
  30.             var bytes = new UTF8Encoding(true, true).GetBytes(secret);
  31.             using (SHA256Managed managed = new SHA256Managed())
  32.             {
  33.                 return managed.ComputeHash(bytes);
  34.             }
  35.         }
  36.     }
  37. }

Dicha clase configura el middleware de OWIN para validar tokens JWT (a través del método UseJwtBearerAuthentication). De esta manera delegamos en OWIN la validación de los tokens JWT y ya no es necesario hacer nada en WebApi,  es decir ya no debemos usar el DelegatingHandler. Por supuesto debemos seguir usando [Authorize] para indicar que servicios deben ser llamados de forma segura (con token JWT).

La solución usando OWIN es la recomendada ya que la validación de los tokens JWT se hace antes de que la petición llegue siquiera a WebApi y por lo tanto te permite integrarlo en otros middlewares (p. ej. si usas Nancy con OWIN este mismo código te sirve, mientras que el DelegatingHandler es algo propio de WebApi).

Enviando el token desde el cliente

Y simplemente por completitud del post, el código para enviar el token JWT desde el cliente sería el siguiente:

  1. protected void AddJwtToken(HttpClient client)
  2. {
  3.     client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", TwitterAuthService.Instance.JwtToken);
  4. }

Donde TwitterAuthService.Instance.JwtToken contiene el token devuelto por WAMS.

La posibilidad de delegar en WAMS todo el flujo oauth es muy cómoda y ello no nos impide que nuestra propia WebApi que puede estar en Azure (en un Website) o on-premise esté protegida a través del token JWT que emite WAMS. De esa manera la aplicación cliente tiene un solo token que usa tanto para acceder a los servicios WAMS (si los usase) como al resto de la API WebAPi que ni tiene que estar en Azure.

Espero que este post os sea de utilidad.

ASP.NET WebApi: Subida de ficheros

Buenas! Vamos a ver en este post como podemos tratar la subida de ficheros en WebApi.

En ASP.NET MVC la subida de ficheros la gestiona un model binder para el tipo HttpFilePostedBase, por lo que basta con declarar un parámetro de este tipo de datos en el controlador y automáticamente recibimos el fichero subido.

En WebApi el enfoque es muy distinto: en el controlador no recibimos ningún parámetro con el contenido del fichero. En su lugar usamos la clase MultipartFormDataStreamProvider para leer el fichero subido y guardarlo en disco (ambas cosas a la vez).

Anatomía de una petición http con fichero subido

Antes de nada veamos como es una petición HTTP en la que se suba un fichero. Para ello he creado un HTML como el siguiente:

  1. <form method="post" enctype="multipart/form-data">
  2.     File: <input type="file" name="aFile"><br />
  3.     File: <input type="file" name="aFile"><br />
  4.     <input type="submit" value="Submit">
  5. </form>

Selecciono dos ficheros cualesquiera y capturo la petición generada con fiddler. El resultado (eliminando todo lo que no nos importa) es el siguiente:

  1. Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryQoYjfxGXTHG6DESL
  2.  
  3. ——WebKitFormBoundaryQoYjfxGXTHG6DESL
  4. Content-Disposition: form-data; name="aFile"; filename="jsio.png"
  5. Content-Type: image/png
  6.  
  7. Contenido binario del fichero
  8. ——WebKitFormBoundaryQoYjfxGXTHG6DESL
  9. Content-Disposition: form-data; name="aFile"; filename="logo_mvp.png"
  10. Content-Type: image/png
  11.  
  12. Contenido binario del fichero
  13. ——WebKitFormBoundaryQoYjfxGXTHG6DESL–

Básicamente:

  • El Content-Type debe ser multipart/form-data
  • El Content-Type debe especificar una boundary. La boundary es un cadena que se usa para separar cada valor de la petición (tanto los ficheros como los valores enviados por formdata si los hubiese).
  • Para cada valor:
    • Se coloca el boundary precedido de —
    • Si es un fichero.
      • se coloca un content-disposition que indica (entre otras cosas) el nombre del fichero
      • El conteido binario del fichero
    • Si no es un fichero (p. ej. es el un formdata que viene de un <input type=text>
      • se coloca un content-disposition que indica el nombre del parámetro
      • Se coloca su valor
  • Finalmente se coloca la boundary para finalizar la petición

Enviar peticiones usando HttpClient

Conociendo como es una petición de subida de ficheros, crearla usando HttpClient es muy simple. El siguiente código sube un fichero:

  1. var requestContent = new MultipartFormDataContent();
  2. var imageContent = new StreamContent(stream);
  3. imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/png");
  4. requestContent.Add(imageContent, "image", string.Format("{0:00}.png", idx));

La variable stream es un Stream para acceder al fichero, mientras que la variable idx es un entero que en este caso se usa para dar nombre al fichero subdido (01.png, 02.png, …).

Si capturamos con fiddler como es la petición generada por este código vemos que es como sigue:

  1. POST http://localtest.me:2706/Upload/Photo/568b8c05-aab8-46db-8cbc-aec2a96dec18/2 HTTP/1.1
  2. Content-Type: multipart/form-data; boundary="c609aabb-3872-4d04-a69d-72024c9325a5"
  3. –c609aabb-3872-4d04-a69d-72024c9325a5
  4. Content-Type: image/png
  5. Content-Disposition: form-data; name=image; filename=02.png; filename*=utf-8''02.png

Podemos observar como se ha generado un boundary para nosotros (realmente el valor del boundary no se usa, es solo para separar los campos) y como se genera un Content-Disposition. Es pues una petición equivalente a usar un <input type=”file” /> (cuyo atributo name fuese “image”).

Recibir el fichero en WebApi

Para recibir el fichero subido, necesitamos una acción de un controlador WebApi y usar un MultipartFormDataStreamProvider para guardar el fichero en disco:

  1. var streamProvider = new MultipartFileStreamProvider(uploadFolder);
  2. await Request.Content.ReadAsMultipartAsync(streamProvider);

Este código ya guarda el fichero en el disco. La carpeta usada es la especificada en la variable uploadFolder. De hecho si hubiese varios ficheros subidos a la vez, este código los guarda todos.

En mi caso he enviado una petición con un Content-Disposition cuyo nombre de fichero es 02.png, así que lo suyo sería esperar que en la carpeta especificada por uploadFolder hubiese este fichero. Pero no vais a encontrar ningún fichero llamado así. Por diseño WebApi ignora el valor de Content-Disposition (por temas de seguridad). En su lugar os vais a encontrar con un fichero (o varios) llamados BodyPart y un guid:

image

Por suerte para hacer que WebApi tenga en cuenta el valor del campo Content-Disposition y guarde el fichero con el nombre especificado basta con heredar de MultipartFormDataStreamProvider y reimplementar el método GetLocalFileName:

  1. class MultipartFormDataContentDispositionStreamProvider : MultipartFormDataStreamProvider
  2. {
  3.     public MultipartFormDataContentDispositionStreamProvider(string rootPath) : base(rootPath)
  4.     {
  5.     }
  6.     public MultipartFormDataContentDispositionStreamProvider(string rootPath, int bufferSize) : base(rootPath, bufferSize)
  7.     {
  8.     }
  9.     public override string GetLocalFileName(HttpContentHeaders headers)
  10.     {
  11.         if (headers.ContentDisposition != null)
  12.         {
  13.             return headers.ContentDisposition.FileName;
  14.         }
  15.         return base.GetLocalFileName(headers);
  16.     }
  17. }

Ahora en el controlador instanciamos un objeto MultipartFormDataContentDispositionStreamProvider en lugar del MultipartFormDataStreamProvider y ahora ya se nos guardarán los ficheros con los nombres especificados. Ojo, recuerda que WebApi no hace eso por defecto por temas de seguridad, así que si implementas esta solución valida los nombres de fichero que te envía el cliente.

¡Y ya está! La verdad es que el modelo de WebApi es radicalmente distinto al de ASP.NET MVC pero igual de sencillo y efectivo 😉

Saludos!