Como proteger uma API da Web do ASP.NET [fechada]

397

Desejo criar um serviço Web RESTful usando a API Web do ASP.NET que desenvolvedores de terceiros usarão para acessar os dados do meu aplicativo.

Eu li bastante sobre o OAuth e parece ser o padrão, mas encontrar uma boa amostra com documentação explicando como funciona (e que realmente funciona!) Parece ser incrivelmente difícil (especialmente para um novato no OAuth).

Existe um exemplo que realmente cria e funciona e mostra como implementar isso?

Eu baixei várias amostras:

  • DotNetOAuth - a documentação é inútil do ponto de vista de um novato
  • Thinktecture - não é possível fazê-lo construir

Também vi blogs sugerindo um esquema simples baseado em token (como este ) - isso parece reinventar a roda, mas tem a vantagem de ser conceitualmente bastante simples.

Parece que existem muitas perguntas como essa no SO, mas não há boas respostas.

O que todo mundo está fazendo neste espaço?

Craig Shearer
fonte

Respostas:

292

Atualizar:

Adicionei este link à minha outra resposta como usar a autenticação JWT para a API da Web do ASP.NET aqui para qualquer pessoa interessada em JWT.


Conseguimos aplicar a autenticação HMAC para proteger a API da Web e funcionou bem. A autenticação HMAC usa uma chave secreta para cada consumidor que, tanto o consumidor quanto o servidor, sabem hmac hash uma mensagem, o HMAC256 deve ser usado. Na maioria dos casos, a senha com hash do consumidor é usada como uma chave secreta.

A mensagem normalmente é criada a partir de dados na solicitação HTTP, ou mesmo dados personalizados que são adicionados ao cabeçalho HTTP, a mensagem pode incluir:

  1. Registro de data e hora: hora em que a solicitação é enviada (UTC ou GMT)
  2. Verbo HTTP: GET, POST, PUT, DELETE.
  3. publicar dados e string de consulta,
  4. URL

Sob o capô, a autenticação HMAC seria:

O consumidor envia uma solicitação HTTP ao servidor da Web, após criar a assinatura (saída do hmac hash), o modelo da solicitação HTTP:

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

Exemplo para solicitação GET:

GET /webapi.hmac/api/values

User-Agent: Fiddler    
Host: localhost    
Timestamp: Thursday, August 02, 2012 3:30:32 PM 
Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=

A mensagem para o hash para obter assinatura:

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

Exemplo para solicitação POST com string de consulta (a assinatura abaixo não está correta, apenas um exemplo)

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

A mensagem para o hash para obter assinatura

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

Observe que os dados do formulário e a string de consulta devem estar em ordem; portanto, o código no servidor obtém a string de consulta e os dados do formulário para criar a mensagem correta.

Quando a solicitação HTTP chega ao servidor, um filtro de ação de autenticação é implementado para analisar a solicitação para obter informações: verbo HTTP, carimbo de data e hora, uri, dados do formulário e seqüência de caracteres de consulta, com base neles para criar a assinatura (use o hmac hash) com o segredo chave (senha com hash) no servidor.

A chave secreta é obtida do banco de dados com o nome de usuário na solicitação.

Em seguida, o código do servidor compara a assinatura na solicitação com a assinatura criada; se igual, a autenticação é aprovada, caso contrário, falhou.

O código para criar assinatura:

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

Então, como evitar ataques de repetição?

Adicione restrição ao carimbo de data / hora, algo como:

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

(servertime: hora da solicitação chegando ao servidor)

E, armazene em cache a assinatura da solicitação na memória (use o MemoryCache, mantenha o tempo limite). Se a próxima solicitação vier com a mesma assinatura da solicitação anterior, ela será rejeitada.

O código de demonstração é apresentado aqui: https://github.com/cuongle/Hmac.WebApi

cuongle
fonte
2
@ James: apenas o registro de data e hora parece não ser suficiente, durante um curto período de tempo eles podem simular a solicitação e enviados ao servidor, eu apenas editei meu post, use ambos seria o melhor.
cuongle
11
Tem certeza de que isso está funcionando como deveria? você está fazendo o hash do registro de data e hora com a mensagem e colocando-a em cache. Isso significaria uma assinatura diferente para cada solicitação, o que tornaria sua assinatura em cache inútil.
Filip Stas
11
@FilipStas: Parece que eu não receber o seu ponto, a razão para usar Cache aqui é para evitar ataque de retransmissão, nada mais
cuongle
11
@ Chrishr: Você pode consultar [esta página] ( jokecamp.wordpress.com/2012/10/21/… ). Vou atualizar essa fonte breve
cuongle
11
A solução sugerida funciona, mas você não pode impedir ataques do tipo Man-in-the-Middle, para isso é necessário implementar o HTTPS
refatorar
34

Eu sugeriria começar com as soluções mais diretas primeiro - talvez a autenticação básica HTTP simples + HTTPS seja suficiente no seu cenário.

Caso contrário (por exemplo, você não pode usar https ou precisa de um gerenciamento de chaves mais complexo), consulte as soluções baseadas em HMAC, conforme sugerido por outras pessoas. Um bom exemplo dessa API seria o Amazon S3 ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html )

Eu escrevi uma postagem no blog sobre autenticação baseada em HMAC na API da Web do ASP.NET. Ele discute o serviço de API da Web e o cliente da API da Web, e o código está disponível no bitbucket. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Aqui está uma publicação sobre autenticação básica na API da Web: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

Lembre-se de que, se você for fornecer uma API a terceiros, provavelmente também será responsável pelo fornecimento de bibliotecas clientes. A autenticação básica tem uma vantagem significativa aqui, pois é suportada na maioria das plataformas de programação prontas para uso. O HMAC, por outro lado, não é tão padronizado e exigirá implementação personalizada. Estes devem ser relativamente diretos, mas ainda exigem trabalho.

PS. Há também uma opção para usar certificados HTTPS +. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/

Piotr Walat
fonte
23

Você já tentou o DevDefined.OAuth?

Usei-o para proteger meu WebApi com OAuth de duas pernas. Também testei com sucesso com clientes PHP.

É muito fácil adicionar suporte ao OAuth usando esta biblioteca. Veja como você pode implementar o provedor da API da Web do ASP.NET MVC:

1) Obtenha o código fonte do DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - a versão mais recente permite OAuthContextBuilderextensibilidade.

2) Crie a biblioteca e faça referência a ela no seu projeto de API da Web.

3) Crie um construtor de contexto personalizado para suportar a construção de um 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 criar um provedor OAuth: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider . Na última etapa (Acessando o exemplo de recurso protegido), você pode usar este código em seu 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()
            };
    }
}

Eu implementei meu próprio provedor, então não testei o código acima (exceto, é claro, o WebApiOAuthContextBuilderque estou usando no meu provedor), mas deve funcionar bem.

Maksymilian Majer
fonte
Obrigado - vou dar uma olhada nisso, embora, por enquanto, eu tenha lançado minha própria solução baseada em HMAC.
Craig Shearer
11
@ CraigShearer - oi, você diz que criou o seu próprio ... só tinha algumas perguntas se não se importa em compartilhar. Estou em uma posição semelhante, onde tenho uma API da Web MVC relativamente pequena. Os controladores da API ficam ao lado de outros controladores / ações que estão sob autenticação de formulários. A implementação do OAuth parece um exagero quando eu já tenho um provedor de associação que eu poderia usar e só preciso garantir algumas operações. Eu realmente quero uma ação de autenticação que retorne um token criptografado - depois usei o token nas chamadas subseqüentes? qualquer informação bem-vinda antes de eu me comprometer a implementar uma solução de autenticação existente. obrigado!
precisa saber é o seguinte
@Maksymilian Majer - Alguma chance de você compartilhar como implementou o provedor com mais detalhes? Estou tendo problemas para enviar respostas de volta ao cliente.
Jlrolin
21

A API da Web introduziu um Atributo [Authorize]para fornecer segurança. Isso pode ser definido globalmente (global.asx)

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

Ou por controlador:

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

É claro que seu tipo de autenticação pode variar e você pode querer executar sua própria autenticação. Quando isso ocorrer, poderá ser útil herdar o Authorizate Attribute e estendê-lo para atender aos seus 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;
        }
    }
}

E no seu controlador:

[DemoAuthorize]
public class ValuesController : ApiController{

Aqui está um link sobre outra implementação personalizada para autorizações da WebApi:

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

Dalorzo
fonte
Obrigado pelo exemplo @Dalorzo, mas tenho alguns problemas. Eu olhei para o link em anexo, mas seguir essas instruções não funciona. Também encontrei as informações necessárias ausentes. Em primeiro lugar, quando crio o novo projeto, é correto escolher Contas de Usuário Individuais para autenticação? Ou deixo sem autenticação. Também não estou recebendo o erro 302 mencionado, mas estou recebendo um erro 401. Por fim, como passo as informações necessárias da minha visualização para o controlador? Como deve ser minha chamada ajax? Btw, estou usando autenticação de formulários para meus modos de exibição MVC. Isso é um problema?
21415 Amanda Amanda
Está funcionando de maneira fantástica. É bom aprender e começar a trabalhar em nossos próprios tokens de acesso.
CodeName47
Um pequeno comentário - tenha cuidado AuthorizeAttribute, pois existem duas classes diferentes com o mesmo nome em espaços para nome diferentes: 1. System.Web.Mvc.AuthorizeAttribute -> para controladores MVC 2. System.Web.Http.AuthorizeAttribute -> para WebApi.
Vitaliy Markitanov
5

Se você deseja proteger sua API em um servidor para servidor (sem redirecionamento para o site para autenticação de duas pernas). Você pode consultar o protocolo de concessão de credenciais do cliente OAuth2.

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

Eu desenvolvi uma biblioteca que pode ajudá-lo a adicionar facilmente esse tipo de suporte à sua WebAPI. Você pode instalá-lo como um pacote NuGet:

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

A biblioteca tem como alvo o .NET Framework 4.5.

Depois de adicionar o pacote ao seu projeto, ele criará um arquivo leia-me na raiz do seu projeto. Você pode olhar para esse arquivo leia-me para ver como configurar / usar este pacote.

Felicidades!

Varun Chatterji
fonte
5
Você está compartilhando / fornecendo código-fonte para essa estrutura como código-fonte aberto?
Barrypicker #
JFR: Primeira ligação é interrompida e um pacote NuGet não foi atualizado
Abdul Qayyum
3

na continuação da resposta de @ Cuong Le, minha abordagem para evitar ataques de repetição seria

// Criptografa o horário do Unix no lado do cliente usando a chave privada compartilhada (ou a senha do usuário)

// Envia como parte do cabeçalho da solicitação para o servidor (WEB API)

// Descriptografar a hora do Unix no servidor (API WEB) usando a chave privada compartilhada (ou a senha do usuário)

// Verifique a diferença horária entre a Hora Unix do Cliente e a Hora Unix do Servidor, não deve ser maior que x s

// se a senha de ID do usuário / hash estiver correta e o UnixTime descriptografado estiver dentro de x segundos do tempo do servidor, é uma solicitação válida

refatorar
fonte