Cómo asegurar una API web ASP.NET [cerrado]

397

Quiero crear un servicio web RESTful utilizando la API web ASP.NET que los desarrolladores externos 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 eso 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 desesperada desde la perspectiva de un novato
  • Thinktecture: no se puede construir

También he mirado blogs que sugieren un esquema simple basado en tokens (como este ): 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?

Craig Shearer
fuente

Respuestas:

292

Actualizar:

He agregado este enlace a mi otra respuesta sobre cómo usar la autenticación JWT para la API web ASP.NET aquí para cualquier persona interesada en JWT.


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

El mensaje normalmente se crea 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 esa solicitud (UTC o GMT)
  2. Verbo HTTP: GET, POST, PUT, DELETE.
  3. publicar datos y consultar cadena,
  4. URL

Bajo el capó, la autenticación HMAC sería:

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

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

Ejemplo de 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 a 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 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, por lo que el código del servidor obtiene la cadena de consulta y los datos del 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 para obtener información: verbo HTTP, marca de tiempo, uri, datos de formulario y cadena de consulta, luego se basa en estos para construir la firma (use 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 construida; 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 evitar el ataque de repetición?

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

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

(tiempo de servicio: hora de solicitud que llega al servidor)

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

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

cuongle
fuente
2
@James: solo la marca de tiempo no parece suficiente, durante poco tiempo pueden simular la solicitud y enviarla al servidor, acabo de editar mi publicación, usar ambas sería lo mejor.
cuongle
1
¿Estás seguro de que esto funciona como debería? está mezclando la marca de tiempo con el mensaje y almacenando en caché ese mensaje. Esto significaría una firma diferente en cada solicitud, lo que haría que su firma en caché sea inútil.
Filip Stas
1
@FilipStas: parece que no entiendo su punto, la razón para usar Cache aquí es para evitar el ataque de retransmisión, nada más
cuongle
1
@ChrisO: Puede consultar [esta página] ( jokecamp.wordpress.com/2012/10/21/… ). Actualizaré esta fuente pronto
cuongle
1
La solución sugerida funciona, pero no puede evitar el ataque Man-in-the-Middle, para eso debe implementar HTTPS
refactorizar el
34

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

Si no es así (por ejemplo, no puede usar https o necesita una administración de claves más compleja), puede echar un vistazo a las soluciones basadas en HMAC como lo sugieren 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 autenticación basada en HMAC en ASP.NET Web API. Analiza 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 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, lo más probable es que también sea responsable de entregar las bibliotecas del cliente. La autenticación básica tiene una ventaja significativa aquí, ya que es compatible con la mayoría de las plataformas de programación listas para usar. HMAC, por otro lado, no está tan estandarizado y requerirá una implementación personalizada. Estos deben 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/

Piotr Walat
fuente
23

¿Has probado DevDefined.OAuth?

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

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

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

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

3) Cree un generador de contexto personalizado para apoyar la creación de un contexto a partir de 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<string> values;

        if (!request.Headers.TryGetValues("Set-Cookie", out values))
        {
            return new NameValueCollection();
        }

        var header = values.FirstOrDefault();

        return this.CollectCookiesFromHeaderString(header);
    }

    /// <summary>
    /// Adjust the URI to match the RFC specification (no query string!!).
    /// </summary>
    /// <param name="uri">
    /// The original URI. 
    /// </param>
    /// <returns>
    /// The adjusted URI. 
    /// </returns>
    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 AuthorizationFilterAttributeatributo:

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

He implementado mi propio proveedor, así que no he probado el código anterior (excepto, por supuesto, el WebApiOAuthContextBuilderque estoy usando en mi proveedor) pero debería funcionar bien.

Maksymilian Majer
fuente
Gracias. Echaré un vistazo a esto, aunque por ahora he desarrollado mi propia solución basada en HMAC.
Craig Shearer
1
@CraigShearer: hola, dices que has hecho el tuyo ... solo tuve algunas preguntas si no te importa compartir. Estoy en una posición similar, donde tengo una API web MVC relativamente pequeña. Los controladores de API se encuentran junto a otros controladores / acciones que se encuentran en formularios de autenticación. Implementar OAuth parece una exageración cuando ya tengo un proveedor de membresía que podría usar y solo necesito asegurar un puñado de operaciones. Realmente quiero una acción de autenticación que devuelva un token cifrado, ¿luego usó el token en llamadas posteriores? cualquier información es bienvenida antes de comprometerme a implementar una solución de autenticación existente. ¡Gracias!
sambomartin
@Maksymilian Majer: ¿hay alguna posibilidad de que pueda compartir cómo ha implementado el proveedor con más detalle? Tengo algunos problemas para enviar respuestas al cliente.
jlrolin
21

Web API introdujo un atributo [Authorize]para proporcionar seguridad. Esto se puede configurar 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 útil heredar de Authorizate Attribute y extenderlo 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 las autorizaciones de WebApi:

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

Dalorzo
fuente
Gracias por el ejemplo @Dalorzo, pero tengo algunos problemas. Miré el enlace adjunto, pero seguir esas instrucciones no funciona del todo. También encontré la información necesaria que falta. En primer lugar, cuando creo el nuevo proyecto, ¿es correcto elegir Cuentas de usuario individuales para la autenticación? ¿O lo dejo sin autenticación? Tampoco recibo el error 302 mencionado, pero recibo un error 401. Por último, ¿cómo paso la información necesaria desde mi vista al controlador? ¿Cómo debe ser mi llamada ajax? Por cierto, estoy usando autenticación de formularios para mis vistas MVC. ¿Es eso un problema?
Amanda
Está funcionando fantásticamente. Simplemente agradable de aprender y comenzar a trabajar en nuestros propios tokens de acceso.
CodeName47
Un pequeño comentario: tenga cuidado AuthorizeAttribute, ya que hay dos clases diferentes con el mismo nombre, en diferentes espacios de nombres: 1. System.Web.Mvc.AuthorizeAttribute -> para controladores MVC 2. System.Web.Http.AuthorizeAttribute -> para WebApi.
Vitaliy Markitanov
5

Si desea asegurar su API de un servidor a otro (sin 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. Puede instalarlo como un paquete NuGet:

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

La biblioteca se dirige 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 mirar ese archivo readme para ver cómo configurar / usar este paquete.

¡Salud!

Varun Chatterji
fuente
55
¿Está compartiendo / proporcionando código fuente para este marco como código abierto?
barrypicker
JFR: El primer enlace está roto y el paquete NuGet nunca se actualizó
abdul qayyum
3

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

// Cifre el tiempo de Unix en el lado del cliente utilizando la clave privada compartida (o la contraseña del usuario)

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

// Descifra la hora de Unix en el servidor (API WEB) usando la clave privada compartida (o la contraseña del usuario)

// Compruebe que la diferencia horaria entre la hora de Unix del cliente y la hora de Unix del servidor no debe ser mayor que x segundos

// si el ID de usuario / contraseña de hash son correctos y el UnixTime descifrado está dentro de x segundos del tiempo del servidor, entonces es una solicitud válida

refactor
fuente