ASP.NET Web API - Segurança

Israel Aece

Julho 2013

Como qualquer outro tipo de aplicação, serviços também devem lidar com segurança. E quando falamos em segurança sempre abordamos dois itens: autenticação e autorização. A autenticação é o processo que consiste em saber quem o usuário é, se ele está corretamente cadastrado e configurado, enquanto a autorização determina se esse usuário possui permissões para acessar o recurso que ele está solicitando. A autenticação, obrigatoriamente, sempre ocorre antes da autorização, pois não há como avaliar/conceder permissões sem antes saber quem ele é.

E como se não bastasse isso, quando estamos lidando com aplicações que encaminham mensagens de um lado para outro, é necessário também nos preocuparmos com a proteção da mesma enquanto ela viaja de um lado ao outro. Neste ponto o protocolo HTTPS ajuda bastante, já que ele é popularmente conhecido e a grande maioria dos hosts e clientes sabem lidar com ele.

Antes de falar sobre as peculiaridades do ASP.NET Web API, precisamos entender alguns conceitos de segurança que existem dentro da plataforma .NET desde a versão 1.0. Duas interfaces são utilizadas como base para os mecanismos de autenticação e autorização: IIdentity e IPrincipal (namespace System.Security.Principal), respectivamente. A interface IIdentity fornece três propriedades autoexplicativas: Name, AuthenticationType e IsAuthenticated. Já a segunda possui dois membros que merecem uma atenção especial. O primeiro deles é a propriedade Identity que retorna a instância de uma classe que implemente a interface IIdentity, representando a identidade do usuário; já o segundo membro trata-se de um método chamado IsInRole que, dado uma papel, retorna um valor boleano indicando se o usuário corrente possui ou não aquele papel. Como podemos notar, as classes de autenticação e autorização trabalham em conjunto.

Dentro do namespace System.Threading existe uma classe chamada Thread. Essa classe determina como controlar uma thread dentro da aplicação. Essa classe, entre vários membros, possui uma propriedade estática chamada CurrentPrincipal que recebe e retorna uma instância de um objeto que implementa a interface IPrincipal. É através desta propriedade que devemos definir qual será a identity e principal que irá representar o contexto de segurança para a thread atual.

Há algumas implementações das interfaces IIdentity e IPrincipal dentro do .NET Framework, como é o caso das classes GenericIdentity, WindowsIdentity, GenericPrincipal e WindowsPrincipal. Essas classes são utilizadas, principalmente, quando queremos implementar no sistema o mecanismo de autorização baseado em papéis, que dado um papel (muitas vezes um departamento), indica se o usuário pode ou não acessar o recurso. Para refinar melhor isso, ao invés de nomearmos por departamento, podemos definir as seções e funcionalidades do sistema que ele poderá acessar/executar.

Em qualquer tipo de projeto ASP.NET, há uma classe chamada HttpContext. Como o próprio nome sugere, essa classe expõe uma série de recursos que estarão acessível por todo o ciclo da requisição dentro do servidor, e um desses recursos é o contexto de segurança do usuário, que através da propriedade User podemos ter acesso ao objeto (IPrincipal) que define a credencial do usuário corrente. Essa propriedade também estará acessível quando hospedar a API em um modelo de web-hosting. Quando utilizarmos o self-hosting, temos que recorrer a propriedade CurrentPrincipal da classe Thread. Se precisarmos hospedar a API em ambos os locais, temos que definir a credencial em ambos os locais, mas no caso do HttpContext, temos que nos certificar que a propriedade estática Current não seja nula, pois ela será se estiver em modo self-hosting.

Há diversas formas de trabalhar com autenticação no ASP.NET Web API, onde a maioria já são bastante conhecidas pelos desenvolvedores e padrões já estabelecidos no mercado. Abaixo temos as principais opções:

  • Basic: A autenticação Basic faz parte da especificação do protocolo HTTP, que define um modelo simples (básico) para transmissão de nome de usuário e senha nas requisições. A sua simplicidade é tão grande quanto a sua insegurança. Por não haver qualquer meio de proteção (hash, criptografia, etc.), obriga a sempre trafegar essas requisições recorrendo à segurança do protocolo, e para isso, seremos obrigados a utilizarmos HTTPS.
  • Digest: Também parte da especificação do protocolo HTTP, é uma modelo mais seguro quando comparado ao Basic, pois apenas o hash da senha (MD5) é enviado ao serviço.
  • Windows: Como o próprio nome diz, a autenticação é baseada nas credenciais do usuário que está acessando o recurso, baseando-se em uma conta no Windows (Active Directory). Entretanto isso é apenas útil quando estamos acessando a partir de uma intranet onde conseguimos ter um ambiente controlado e homogêneo.
  • Forms: Desenhado para a internet a autenticação baseada em forms está presente no ASP.NET desde a sua versão 1.0, e é baseada em cookies.

Até então somente foi falado sobre os tipos de autenticação, o gerenciamento da identidade e dos objetos que temos a disposição e que representam o usuário, mas não de como e quando configurar isso. A escolha dependerá de como e onde quer (re)utilizar esse mecanismo de autenticação. Se quisermos avaliar em qualquer modelo de hosting, então a melhor opção é recorrer ao uso de message handlers, que trata-se de um pouco de estensibilidade do ASP.NET Web API e que pode rodar para todas as requisições ou para um determinada rota. Haverá um capítulo para esgotar este assunto.

Para exemplificar vamos nos basear na autenticação Basic. A ideia é criar um handler para interceptar a requisição e extrair o header do HTTP que representa a credencial informada pelo usuário (WWW-Authenticate), e a valida em algum repositório de sua escolha, como uma base de dados. A partir daqui é necessário conhecermos como funciona o processo deste modelo, para conseguirmos dialogar com o cliente, para que assim ele consiga coordenar o processo de autenticação do usuário.

  • O cliente solicita um recurso (página, serviço, etc.) que está protegido.
  • Ao detectar que o cliente não está autenticado, o servidor exige que ele se autentique e informe as credenciais a partir do modelo Basic. Isso é informado a partir de um header chamado WWW-Authenticate: Basic.
  • Neste momento, o servidor retorna uma resposta com o código 401 (Unauthorized), que instrui o cliente (navegador) a exigir as credenciais de acesso do usuário que está acessando o recurso.
  • Uma vez informado, o browser recria a mesma requisição, mas agora envia nos headers da mesma o login e senha codificados em Base64, sem qualquer espécie de criptografia. Na resposta, o header enviado é o Authorization: Basic [Username+Password Codificado].
  • Quando este header acima estiver presente, o servidor (IIS) é capaz de validá-lo no Windows/Active Directory, e se for um usuário válido, permitirá o acesso, caso contrário retornará a mesma resposta com código 401, até que ele digite uma credencial válida.

Como dito acima, o login e a senha não são enviados até que sejam efetivamente exigidos. Nesta customização, ao identificarmos que o header não está presente na requisição, precisamos configurar a resposta para o cliente com o código 401, que representa acesso não autorizado, e informar na resposta o mesmo header, para continuar obrigando o usuário a informar o login e senha.

Para saber se o cliente informou as credenciais, precisamos detectar a presença do header chamado Authorization. Se existir, então precisamos decodificá-lo, utilizando o método FromBase64String da classe Convert, que dado uma string, retorna um array de bytes representando as credenciais separadas por um ":". Depois disso, tudo o que precisamos fazer é separá-los, para que assim podermos efetuar a validação em algum repositório.

Depois de conhecer um pouco mais sobre o processo que ocorre entre cliente e serviço, vamos implementar isso no ASP.NET Web API utilizando um message handler, que é a forma que temos para interceptar as requisições que chegam para a API.

public class AuthenticationHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        string username = null;

        if (IsValid(request, out username))
        {
            var principal = new GenericPrincipal(new GenericIdentity(username), null);
            Thread.CurrentPrincipal = principal;

            if (HttpContext.Current != null)
                HttpContext.Current.User = principal;

            return base.SendAsync(request, cancellationToken);
        }
        else
        {
            return Task.Factory.StartNew(() =>
            {
                var r = new HttpResponseMessage(HttpStatusCode.Unauthorized);
                r.Headers.Add("WWW-Authenticate", "Basic realm=\"AppTeste\"");
                return r;
            });
        }
    }
}

Ao recepcionar a requisição e ela for válida, antes dele encaminhar a mesma adiante, para que ela chegue até a ação que o cliente requisitou, ele cria o objeto que definirá a credencial/identidade do usuário que está acessando o recurso. Ao requisitar pela primeira vez e se estivermos consumindo isso em um navegador, ao receber esse código em conjunto com este header, uma janela é aberta para que você informe o login e senha, que serão encaminhados ao serviço.

Dn376307.5ADD47C6178B47942F5BC8199284174F(pt-br,MSDN.10).png

Figura 18 - Solicitação de login e senha pelo browser.

Ao interceptar essa requisição, podemos comprovar tudo o que foi escrito acima. Temos a resposta contendo o código 401, e depois de informado o login e senha, a nova requisição com o header Authorization contendo o valor codificado em Base64.

Dn376307.471B381A27AC96ED66B1D276138D0489(pt-br,MSDN.10).png

Figura 19 - Headers referente à autenticação Basic.

private static bool IsValid(HttpRequestMessage request, out string username)
{
    username = null;
    var header = request.Headers.Authorization;

    if (header != null && header.Scheme == "Basic")
    {
        var credentials = header.Parameter;

        if (!string.IsNullOrWhiteSpace(credentials))
        {
            var decodedCredentials =
                Encoding.Default.GetString(Convert.FromBase64String(credentials));

            var separator = decodedCredentials.IndexOf(':');
            var password = decodedCredentials.Substring(separator + 1);

            username = decodedCredentials.Substring(0, separator);

            return username == password; //Validação em algum repositório
        }
    }

    return false;
}

A classe ApiController fornece uma propriedade chamada User, que retorna um objeto do tipo IPrincipal. Isso quer dizer que, se precisar extrair as credenciais do usuário dentro do método/ação, podemos recorrer a ela. E, para finalizar, é necessário incluir este handler na coleção de handlers do serviço, através do arquivo Global.asax:

    config.MessageHandlers.Add(new AuthenticationHandler());

Depois da autenticação finalizada, de sabermos quem é o usuário que está acessando o recurso, chega o momento de controlarmos e sabermos e se ele tem as devidas permissões para acesso, que é o processo de autorização.

O principal elemento que controla o acesso é o atributo AuthorizeAttribute (namespace System.Web.Http), e pode ser aplicado em um controller inteiro ou individualmente em cada ação, caso precisemos de um controle mais refinado, ou até mesmo, em nível global, onde ele deve ser executado/assegurado independente de qualquer controller ou ação que seja executada dentro daquela aplicação. Este atributo possui duas propriedades: Roles e Users. Cada uma delas recebe um string com o nome dos papéis ou usuários que podem acessar determinado recurso, separados por vírgula.

Pelo fato deste atributo ser um filtro (falaremos mais sobre eles abaixo), ele é executado diretamente pela infraestrutura do ASP.NET Web API, que irá assegurar que o usuário está acessando está dentro daqueles nomes colocados na propriedade Users ou que ele esteja contido em algum dos papéis que são colocados na propriedade Roles. E como já era de se esperar, ele extrai as informações do usuário da propriedade CurrentPrincipal da classe Thread, qual definimos durante o processo de autenticação, dentro do método SendAsync do handler criado acima.

Depois de saber quem o usuário é, podemos extrair as permissões que ele possui, que provavelmente estarão armazenadas em algum repositório também. No exemplo que vimos acima da autenticação, há um segundo parâmetro na classe GenericPrincipal que é um array de strings, que representam os papéis do usuário. Abaixo temos aquele mesmo código ligeiramente alterado para buscar pelas permissões do usuário:

var principal = 
    new GenericPrincipal(
        new GenericIdentity(username), 
        CarregarPermissoes(username));

//....

private static string[] CarregarPermissoes(string username)
{
    if (username == "Israel")
        return new[] { "Admin", "IT" };

    return new[] { "Normal" };
}

Como comentado acima, temos três níveis que podemos aplicar o atributo AuthorizeAttribute: no método, no controller e em nível global. O código abaixo ilustra o uso destas três formas de utilização:

//Nível de Método
public class ClientesController : ApiController
{
    [Authorize(Roles = "Admin, IT")]
    public IEnumerable<Cliente> Get()
    {
        //...
    }
}

//Nível de Controller
[Authorize(Roles = "Admin")]
public class ClientesController : ApiController
{
    public IEnumerable<Cliente> Get()
    {
        //...
    }
}

//Nível Global
config.Filters.Add(new AuthorizeAttribute());

E se quisermos refinar ainda mais a autorização, podemos levar a validação disso para dentro do método. Basta remover o atributo e recorrer ao método IsInRole através da propriedade User, que dado o nome do papel, retorna um valor boleano indicando se o usuário atual está ou não contido nele.

public IEnumerable<Cliente> Get()
{
    if (User.IsInRole("Admin"))
        //Retornar todos os clientes.

    //Retornar apenas os clientes da carteira 
}

Somente o fato de utilizar o atributo AuthorizeAttribute aplicado em alguns dos níveis que vimos, é o suficiente para que o ASP.NET Web API consiga assegurar que ele somente aquele determinado método/controller se ele estiver autenticado, independente dos papéis que ele possua. Se quisermos flexibilizar o acesso à alguns métodos, podemos recorrer ao atributo AllowAnonymousAttribute para conceder o acesso à um determinado método, mesmo que o controller esteja marcado com o atributo AuthorizeAttribute.

Veja também:

ASP.NET Web API – HTTP, REST e o ASP.NET: Para basear todas as funcionalidades expostas pela tecnologia, precisamos ter um conhecimento básico em relação ao que motivou tudo isso, contando um pouco da história e evolução, passando pela estrutura do protocolo HTTP e a relação que tudo isso tem com o ASP.NET.

ASP.NET Web API – Estrutura da API: Entenderemos aqui a template de projeto que o Visual Studio fornece para a construção das APIs, bem como sua estrutura e como ela se relaciona ao protocolo.

ASP.NET Web API – Roteamento: Como o próprio nome diz, o capítulo irá abordar a configuração necessária para que a requisição seja direcionada corretamente para o destino solicitado, preenchendo e validando os parâmetros que são por ele solicitado.

ASP.NET Web API – Hosting: Um capítulo de extrema relevância para a API. É o hosting que dá vida à API, disponibilizando para o consumo por parte dos clientes, e a sua escolha interfere diretamente em escalabilidade, distribuição e gerenciamento. Existem diversas formas de se expor as APIs, e aqui vamos abordar as principais delas.

ASP.NET Web API – Consumo: Como a proposta é ter uma API sendo consumido por qualquer cliente, podem haver os mais diversos meios (bibliotecas) de consumir estas APIs. Este capítulo tem a finalidade de exibir algumas opções que temos para este consumo, incluindo as opções que a Microsoft criou para que seja possível efetuar o consumo por aplicações .NET.

ASP.NET Web API – Formatadores: Os formatadores desempenham um papel importante na API. São eles os responsáveis por avaliar a requisição, extrair o seu conteúdo, e quando a resposta é devolvida ao cliente, ele entra em ação novamente para formatar o conteúdo no formato em que o cliente possa entender. Aqui vamos explorar os formatadores padrões que já estão embuitdos, bem como a criação de um novo.

ASP.NET Web API – Segurança: Como a grande maioria das aplicações, temos também que nos preocupar com a segurança das APIs. E quando falamos de aplicações distribuídas, além da autenticação e autorização, é necessário nos preocuparmos com a segurança das mensagens que são trocadas entre o cliente e o serviço. Este capítulo irá abordar algumas opções que temos disponíveis para tornar as APIs mais seguras.

ASP.NET Web API – Testes e Tracing: Para toda e qualquer aplicação, temos a necessidade de escrever testes para garantir que a mesma se comporte conforme o esperado. Isso não é diferentes com APIs Web. Aqui iremos abordar os recursos, incluindo a própria IDE, para a escrita, gerenciamento e execução dos testes.

ASP.NET Web API – Estensibilidade e Arquitetura: Mesmo que já tenhamos tudo o que precisamos para criar e consumir uma API no ASP.NET Web API, a customização de algum ponto sempre acaba sendo necessária, pois podemos criar mecanismos reutilizáveis, “externalizando-os” do processo de negócio em si. O ASP.NET Web API foi concebido com a estensibilidade em mente, e justamente por isso que existe um capítulo exclusivo para abordar esse assunto.

| Home | Artigos Técnicos | Comunidade