Cómo proteger una API web de ASP.NET

Quiero construir un servicio web RESTful utilizando ASP.NET Web API que los desarrolladores de terceros utilizarán para acceder a los datos de mi aplicación.

He leído mucho sobre OAuth y parece ser el estándar, pero encontrar una buena muestra con documentación que explique cómo funciona (¡y que realmente funciona!) Parece ser increíblemente difícil (especialmente para un novato en OAuth).

¿Hay una muestra que realmente construye y funciona y muestra cómo implementar esto?

He descargado numerosas muestras:

  • DotNetOAuth: la documentación es inútil desde la perspectiva de un novato
  • Thinktecture: no se puede construir

También he observado blogs que sugieren un esquema simple basado en token (como este ): esto parece reinventar la rueda, pero tiene la ventaja de ser conceptualmente bastante simple.

Parece que hay muchas preguntas como esta en SO, pero no hay buenas respuestas.

¿Qué están haciendo todos en este espacio?

Actualizar:

He puesto mi otra respuesta sobre cómo usar la autenticación JWT para la API web aquí para cualquier persona interesada en JWT:

Autenticación JWT para Asp.Net Web Api


Hemos logrado aplicar la autenticación HMAC para proteger la API web, y funcionó bien. La autenticación HMAC utiliza una clave secreta para cada consumidor, que tanto el consumidor como el servidor saben que debe enviar un mensaje HMAC256. La mayoría de los casos, la contraseña hash del consumidor se usa como clave secreta.

El mensaje normalmente se genera a partir de datos en la solicitud HTTP, o incluso datos personalizados que se agregan al encabezado HTTP, el mensaje puede incluir:

  1. Marca de tiempo: hora en que se envía la solicitud (UTC o GMT)
  2. Verbo HTTP: GET, POST, PUT, DELETE.
  3. publicar datos y cadena de consulta,
  4. URL

Debajo del capó, la autenticación HMAC sería:

El consumidor envía una solicitud HTTP al servidor web, después de comstackr la firma (salida de hmac hash), la plantilla de solicitud HTTP:

User-Agent: {agent} Host: {host} Timestamp: {timestamp} Authentication: {username}:{signature} 

Ejemplo para solicitud GET:

 GET /webapi.hmac/api/values User-Agent: Fiddler Host: localhost Timestamp: Thursday, August 02, 2012 3:30:32 PM Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw= 

El mensaje al hash para obtener la firma:

 GET\n Thursday, August 02, 2012 3:30:32 PM\n /webapi.hmac/api/values\n 

Ejemplo de solicitud POST con cadena de consulta (la firma a continuación no es correcta, solo un ejemplo)

 POST /webapi.hmac/api/values?key2=value2 User-Agent: Fiddler Host: localhost Content-Type: application/x-www-form-urlencoded Timestamp: Thursday, August 02, 2012 3:30:32 PM Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw= key1=value1&key3=value3 

El mensaje a hash para obtener la firma

 GET\n Thursday, August 02, 2012 3:30:32 PM\n /webapi.hmac/api/values\n key1=value1&key2=value2&key3=value3 

Tenga en cuenta que los datos del formulario y la cadena de consulta deben estar en orden, de modo que el código en el servidor obtenga cadena de consulta y datos de formulario para generar el mensaje correcto.

Cuando la solicitud HTTP llega al servidor, se implementa un filtro de acción de autenticación para analizar la solicitud de obtención de información: verbo HTTP, marca de tiempo, uri, datos de formulario y cadena de consulta, y luego basarse en éstos para generar firma (usar hash hmac) con el secreto clave (contraseña hash) en el servidor.

La clave secreta se obtiene de la base de datos con el nombre de usuario en la solicitud.

Luego, el código del servidor compara la firma en la solicitud con la firma incorporada; si es igual, se pasa la autenticación; de lo contrario, falla.

El código para construir la firma:

 private static string ComputeHash(string hashedPassword, string message) { var key = Encoding.UTF8.GetBytes(hashedPassword.ToUpper()); string hashString; using (var hmac = new HMACSHA256(key)) { var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message)); hashString = Convert.ToBase64String(hash); } return hashString; } 

Entonces, ¿cómo prevenir el ataque de repetición?

Agregue restricción para la marca de tiempo, algo como:

 servertime - X minutes|seconds <= timestamp <= servertime + X minutes|seconds 

(Servertime: tiempo de solicitud llegando al servidor)

Y, almacenar en caché la firma de la solicitud en la memoria (use MemoryCache, debe mantenerse en el límite de tiempo). Si la próxima solicitud viene con la misma firma con la solicitud anterior, será rechazada.

El código de demostración se pone como aquí: https://github.com/cuongle/Hmac.WebApi

Sugeriría comenzar primero con las soluciones más sencillas: tal vez basta con la autenticación HTTP básica básica + HTTPS en su escenario.

Si no es así (por ejemplo, no puede usar https, o necesita una administración de claves más compleja), puede echarle un vistazo a las soluciones basadas en HMAC según lo sugerido por otros. Un buen ejemplo de dicha API sería Amazon S3 ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html )

Escribí una publicación de blog sobre la autenticación basada en HMAC en ASP.NET Web API. Discute tanto el servicio de API web como el cliente de API web y el código está disponible en bitbucket. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Aquí hay una publicación sobre Autenticación básica en la API web: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

Recuerde que si va a proporcionar una API a terceros, también es probable que sea responsable de la entrega de las bibliotecas de los clientes. La autenticación básica tiene una ventaja significativa aquí, ya que es compatible con la mayoría de las plataformas de progtwigción listas para usar. HMAC, por otro lado, no está tan estandarizado y requerirá una implementación personalizada. Estos deberían ser relativamente sencillos pero aún requieren trabajo.

PD. También hay una opción para usar certificados HTTPS +. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/

¿Has probado DevDefined.OAuth?

Lo he usado para asegurar mi WebApi con OAuth de 2 patas. También lo he probado con éxito con clientes de PHP.

Es bastante fácil agregar soporte para OAuth usando esta biblioteca. A continuación, le mostramos cómo puede implementar el proveedor de ASP.NET MVC Web API:

1) Obtenga el código fuente de DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth – la versión más reciente permite la extensibilidad de OAuthContextBuilder .

2) Cree la biblioteca y haga referencia a ella en su proyecto de API web.

3) Cree un generador de contexto personalizado para admitir la construcción de un contexto desde HttpRequestMessage :

 using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using System.Web; using DevDefined.OAuth.Framework; public class WebApiOAuthContextBuilder : OAuthContextBuilder { public WebApiOAuthContextBuilder() : base(UriAdjuster) { } public IOAuthContext FromHttpRequest(HttpRequestMessage request) { var context = new OAuthContext { RawUri = this.CleanUri(request.RequestUri), Cookies = this.CollectCookies(request), Headers = ExtractHeaders(request), RequestMethod = request.Method.ToString(), QueryParameters = request.GetQueryNameValuePairs() .ToNameValueCollection(), }; if (request.Content != null) { var contentResult = request.Content.ReadAsByteArrayAsync(); context.RawContent = contentResult.Result; try { // the following line can result in a NullReferenceException var contentType = request.Content.Headers.ContentType.MediaType; context.RawContentType = contentType; if (contentType.ToLower() .Contains("application/x-www-form-urlencoded")) { var stringContentResult = request.Content .ReadAsStringAsync(); context.FormEncodedParameters = HttpUtility.ParseQueryString(stringContentResult.Result); } } catch (NullReferenceException) { } } this.ParseAuthorizationHeader(context.Headers, context); return context; } protected static NameValueCollection ExtractHeaders( HttpRequestMessage request) { var result = new NameValueCollection(); foreach (var header in request.Headers) { var values = header.Value.ToArray(); var value = string.Empty; if (values.Length > 0) { value = values[0]; } result.Add(header.Key, value); } return result; } protected NameValueCollection CollectCookies( HttpRequestMessage request) { IEnumerable values; if (!request.Headers.TryGetValues("Set-Cookie", out values)) { return new NameValueCollection(); } var header = values.FirstOrDefault(); return this.CollectCookiesFromHeaderString(header); } ///  /// Adjust the URI to match the RFC specification (no query string!!). ///  ///  /// The original URI. ///  ///  /// The adjusted URI. ///  private static Uri UriAdjuster(Uri uri) { return new Uri( string.Format( "{0}://{1}{2}{3}", uri.Scheme, uri.Host, uri.IsDefaultPort ? string.Empty : string.Format(":{0}", uri.Port), uri.AbsolutePath)); } } 

4) Use este tutorial para crear un proveedor de OAuth: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider . En el último paso (Acceso al ejemplo de recurso protegido) puede usar este código en su atributo AuthorizationFilterAttribute :

 public override void OnAuthorization(HttpActionContext actionContext) { // the only change I made is use the custom context builder from step 3: OAuthContext context = new WebApiOAuthContextBuilder().FromHttpRequest(actionContext.Request); try { provider.AccessProtectedResourceRequest(context); // do nothing here } catch (OAuthException authEx) { // the OAuthException's Report property is of the type "OAuthProblemReport", it's ToString() // implementation is overloaded to return a problem report string as per // the error reporting OAuth extension: http://wiki.oauth.net/ProblemReporting actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized) { RequestMessage = request, ReasonPhrase = authEx.Report.ToString() }; } } 

Implementé mi propio proveedor, por lo que no he probado el código anterior (excepto, por supuesto, el WebApiOAuthContextBuilder que estoy usando en mi proveedor), pero debería funcionar bien.

Web API introdujo un atributo [Authorize] para proporcionar seguridad. Esto puede establecerse globalmente (global.asx)

 public static void Register(HttpConfiguration config) { config.Filters.Add(new AuthorizeAttribute()); } 

O por controlador:

 [Authorize] public class ValuesController : ApiController{ ... 

Por supuesto, su tipo de autenticación puede variar y es posible que desee realizar su propia autenticación; cuando esto ocurra, puede encontrar una herencia útil de Atributo Autorizado y ampliarla para cumplir con sus requisitos:

 public class DemoAuthorizeAttribute : AuthorizeAttribute { public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext) { if (Authorize(actionContext)) { return; } HandleUnauthorizedRequest(actionContext); } protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext) { var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized); challengeMessage.Headers.Add("WWW-Authenticate", "Basic"); throw new HttpResponseException(challengeMessage); } private bool Authorize(System.Web.Http.Controllers.HttpActionContext actionContext) { try { var someCode = (from h in actionContext.Request.Headers where h.Key == "demo" select h.Value.First()).FirstOrDefault(); return someCode == "myCode"; } catch (Exception) { return false; } } } 

Y en tu controlador:

 [DemoAuthorize] public class ValuesController : ApiController{ 

Aquí hay un enlace sobre otra implementación personalizada para autorizaciones de WebApi:

http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-membership-provider/

Si desea proteger su API de servidor a servidor (no hay redirección al sitio web para la autenticación de 2 patas). Puede consultar el protocolo OAuth2 Client Credentials Grant.

https://dev.twitter.com/docs/auth/application-only-auth

He desarrollado una biblioteca que puede ayudarlo a agregar fácilmente este tipo de soporte a su WebAPI. Puedes instalarlo como un paquete NuGet:

https://nuget.org/packages/OAuth2ClientCredentialsGrant/1.0.0.0

La biblioteca apunta a .NET Framework 4.5.

Una vez que agregue el paquete a su proyecto, creará un archivo Léame en la raíz de su proyecto. Puede ver el archivo Léame para ver cómo configurar / usar este paquete.

¡Aclamaciones!

en la continuación de la respuesta de @ Cuong Le, mi enfoque para evitar el ataque de repetición sería

// Cifrar el tiempo Unix en el lado del cliente usando la clave privada compartida (o la contraseña del usuario)

// Enviarlo como parte del encabezado de solicitud al servidor (API WEB)

// Descifrar el tiempo de Unix en el servidor (API WEB) utilizando la clave privada compartida (o la contraseña del usuario)

// Verificar la diferencia de tiempo entre el Tiempo Unix del Cliente y el Tiempo Unix del Servidor, no debe ser mayor que x sec

// si User ID / Hash Password es correcta y el UnixTime descifrado está dentro de x seg de la hora del servidor, entonces es una solicitud válida