ASP.NET

Habilitando e personalizando a segurança de serviços da API Web ASP.NET

Peter Vogel

No cenário mais comum — JavaScript em uma página da Web acessando um serviço da API Web no mesmo site — falar sobre segurança para a API Web ASP.NET é quase redundante. Se você autenticar seus usuários e autorizar o acesso aos Web Forms/Views que contêm o JavaScript que consome seus serviços, é provável que tenha fornecido toda a segurança que seus serviços precisam. Isso é o resultado do envio pelo ASP.NET dos cookies e informações de autenticação que são utilizadas para validar solicitações de página como parte de quaisquer solicitações JavaScript do lado do cliente para seus métodos de serviço. Há uma exceção (e ela é importante): o ASP.NET não o protege automaticamente contra ataques CSRF/XSRF (solicitação intersite forjada) — falaremos sobre isso mais adiante.

Além da CSRF, há dois cenários em que faz sentido conversar sobre a segurança de seus serviços da API Web. O primeiro cenário é quando seu serviço é consumido por um cliente que não é uma página no mesmo site que seus ApiControllers. Esses clientes não seriam autenticados pela Autenticação de Formulários e não obteriam os cookies e tokens que o ASP.NET usa para controlar o acesso aos seus serviços.

O segundo cenário ocorre quando você deseja incluir uma autorização adicional aos seus serviços além daquela fornecida pela segurança do ASP.NET. A autorização padrão que o ASP.NET fornece é baseada na identidade que o ASP.NET atribui à solicitação durante a autenticação. Talvez você deseje estender essa identidade para autorizar o acesso baseado em algo diferente do nome ou da função da identidade.

A API Web oferece uma série de opções para lidar com esses dois cenários. Na verdade, embora fale sobre a segurança no contexto de aceitação de solicitações da API Web, como a API Web é baseada no mesmo fundamento do ASP.NET que o Web Forms e o MVC, as ferramentas que abordarei neste artigo serão familiares para qualquer um que tenha trabalhado com segurança no Web Forms ou MVC.

Há uma limitação: embora a API Web ofereça várias opções de autenticação e autorização, a segurança começa com o host, independentemente se for o IIS ou um host criado por você para hospedagem interna. Se, por exemplo, você desejar garantir a privacidade na comunicação entre um serviço da API Web e o cliente, você deve, no mínimo, ativar o SSL. Essa, porém, é uma responsabilidade do administrador do site, e não do desenvolvedor. Neste artigo, irei ignorar o host para me concentrar no que um desenvolvedor pode — e deve — fazer para proteger um serviço da API Web (e as ferramentas que abordarei aqui funcionam se o SSL estiver ativado ou desativado).

Impedindo ataques de solicitação intersite forjada

Quando um usuário acessa um site do ASP.NET usando a Autenticação de Formulários, o ASP.NET gera um cookie que determina que o usuário está autenticado. O navegador continuará enviando esse cookie em cada solicitação subsequente para o site, independentemente de onde essa solicitação for originada. Isso, assim como qualquer esquema de autenticação no qual o navegador automaticamente envia informações de autenticação recebidas anteriormente, deixa seu site aberto para ataques CSRF. Se o usuário visitar algum site mal-intencionado depois que seu site fornecer ao navegador o cookie de segurança, esse site poderá enviar solicitações para o seu serviço, aproveitando o cookie de autenticação que o navegador recebeu antes.

Para impedir ataques CSRF, você deverá gerar tokens contra falsificação no servidor e inseri-los na página para que sejam usados em suas chamadas do lado do cliente. A Microsoft fornece a classe AntiForgery com um método GetToken que gerará tokens específicos para o usuário que fizer a solicitação (que pode, é claro, ser o usuário anônimo). Este código gera os dois tokens e os coloca na ViewBag do ASP.NET MVC, onde podem ser usados na View:

[Authorize(Roles="manager")]
public ActionResult Index()
{
  string cookieToken;
  string formToken;
  AntiForgery.GetTokens(null, out cookieToken, out formToken);
  ViewBag.cookieToken = cookieToken;
  ViewBag.formToken = formToken;
  return View("Index");
}

Quaisquer chamadas JavaScript para o servidor deverão retornar os tokens como parte da solicitação (um site de CSRF não terá esses tokens e não poderá retorná-los). Este código, em uma View, gera dinamicamente uma chamada JavaScript que adiciona os tokens aos cabeçalhos da solicitação:

$.ajax("http://phvis.com/api/Customers",{
  type: "get",
  contentType: "application/json",
  headers: {
    'formToken': '@ViewBag.formToken',
    'cookieToken': '@ViewBag.cookieToken' }});

Uma solução um pouco mais complexa permitiria que você usasse JavaScript não invasivo ao inserir os tokens em campos ocultos na View. A primeira etapa nesse processo seria adicionar os tokens ao dicionário ViewData:

ViewData["cookieToken"] = cookieToken;
ViewData["formToken"] = formToken;

Agora, na View, você pode inserir os dados em campos ocultos. O método Hidden de HtmlHelper precisa apenas receber o valor de uma chave na ViewDate para gerar a marca de entrada certa.

@Html.Hidden("formToken")

A marca de entrada resultante usará a chave ViewData para o nome e os atributos de ID da marca e colocará os dados recuperados do dicionário ViewData no atributo de valor da marca. A marca de entrada gerada com o código anterior ficaria assim:

    <input id="formToken" name="formToken" type="hidden" value="...token..." />

Em seguida, seu código JavaScript (mantido em um arquivo separado da View) pode recuperar os valores das marcas de entrada e usá-los em sua chamada ajax:

$.ajax("http://localhost:49226/api/Customers", {
  type: "get",
  contentType: "application/json",
  headers: {
    'formToken': $("#formToken").val(),
    'cookieToken': $("#cookieToken").val()}});

Você pode alcançar os mesmos objetivos em um site do ASP.NET Web Forms usando o método RegisterClientScriptBlock no objeto ClientScriptManager (recuperável na propriedade ClientScript de Page) para inserir código JavaScript com os tokens inseridos:

string CodeString = "function CallService(){" +
  "$.ajax('http://phvis.com/api/Customers',{" +
  "type: 'get', contentType: 'application/json'," +
  "headers: {'formToken': '" & formToken & "',” +
  "'cookieToken': '" & cookieToken & "'}});}"
this.ClientScript.RegisterClientScriptBlock(
  typeOf(this), "loadCustid", CodeString, true);

Por fim, você deverá validar os tokens no servidor quando eles forem retornados pela chamada JavaScript. Os usuários do Visual Studio 2012 que tiverem aplicado a atualização do ASP.NET e do Web Tools 2012.2 verão que o novo modelo de SPA (Aplicativo de Página Única) inclui um filtro ValidateHttpAntiForgeryToken que pode ser usado em métodos da API Web. Na ausência desse filtro, você deverá recuperar os tokens e passá-los para o método Validate da classe AntiForgery (o método Validate gerará uma exceção se os tokens não forem válidos ou tiverem sido gerados para um usuário diferente). O código na Figura 1, usado em um método de serviço da API Web, recupera os tokens dos cabeçalhos e os valida.

Figura 1 Validando tokens CSRF em um método de serviço

public HttpResponseMessage Get(){
  if (Request.Headers.TryGetValues("cookieToken", out tokens))
  {
    string cookieToken = tokens.First();
    Request.Headers.TryGetValues("formToken", out tokens);
    string formToken = tokens.First();
    AntiForgery.Validate(cookieToken, formToken);
  }
  else
  {
    HttpResponseMessage hrm =
      new HttpResponseMessage(HttpStatusCode.Unauthorized);
    hrm.ReasonPhrase = "CSRF tokens not found";
    return hrm;
  } 
  // ... Code to process request ...

O uso de ValidateHttpAntiForgeryToken (em vez do código dentro do método) antecipa o processamento no ciclo (antes, por exemplo, de associação de modelos), o que é algo bom.

Por que nenhum OAuth?

Este artigo deliberadamente ignora OAuth. A especificação OAuth define como os tokens podem ser recuperados por um cliente a partir de um servidor de terceiros para serem enviados a um serviço que irá, por sua vez, validá-los com o servidor de terceiros. Uma discussão sobre como acessar um provedor de token OAuth no cliente ou no serviço está além do escopo deste artigo.

A versão inicial do OAuth também não é uma boa correspondência para a API Web. Aparentemente, um dos principais motivos para usar a API Web é para a utilização de solicitações mais leves baseadas em REST e JSON. Esse objetivo torna a primeira versão do OAuth uma opção pouco atraente para serviços da API Web. Os tokens especificados pela primeira versão do OAuth são pesados e baseados em XML. Felizmente, o OAuth 2.0 apresentou uma especificação para um token JSON mais leve que é mais compacto do que o token das versões anteriores. Tudo indica que as técnicas discutidas neste artigo possam ser usadas para processar quaisquer tokens OAuth enviados ao seu serviço.

Autenticação básica

A primeira das duas responsabilidades principais que você tem ao proteger um serviço da API Web é a autenticação (a outra responsabilidade é a autorização). Vou pressupor que outras questões — privacidade, por exemplo — são tratadas no host.

Em condições ideais, a autenticação e a autorização serão realizadas o quanto antes no pipeline da API Web para evitar passar ciclos de processamento em uma solicitação que você pretende negar. As soluções de autenticação deste artigo são usadas logo no início do pipeline — praticamente assim que a solicitação é recebida. Essas técnicas também permitem integrar a autenticação a quaisquer listas de usuários que você já estiver mantendo. As técnicas de autorização discutidas podem ser aplicadas em uma variedade de locais no pipeline (incluindo no final, no próprio método de serviço) e podem trabalhar com a autenticação para autorizar solicitações baseadas em critérios diferentes do nome ou da função do usuário.

Você pode oferecer suporte a clientes que não passaram pela Autenticação de formulários, fornecendo seu próprio método de autenticação em um módulo HTTP personalizado (ainda presumo que você não esteja autenticando com base em contas do Windows, mas com base em sua própria lista de usuários válidos). Existem dois benefícios principais na utilização de um módulo HTTP: um módulo participa de registro em log e auditoria HTTP; além disso, módulos são chamados bem cedo no pipeline. Embora ambas sejam coisas boas, é preciso pagar dois preços para usar módulos: os módulos são globais e são aplicados a todas as solicitações para o site, não apenas às solicitações da API Web; além disso, para usar os módulos de autenticação, você deve hospedar seu serviço no IIS. Posteriormente, neste artigo, falarei sobre o uso de manipuladores de delegação que são chamados apenas para as solicitações da API Web e são indiferentes ao host.

Para esse exemplo de uso de um módulo HTTP, presumo que o IIS está usando autenticação básica e que as credenciais usadas para autenticar um usuário são nome de usuário e senha, enviados pelo cliente (neste artigo, irei ignorar a certificação do Windows, mas falarei sobre o uso de certificados do cliente). Também presumo que o serviço da API Web para o qual estou criando a segurança é protegido usando um atributo Authorize, como este, que especifica um usuário:

public class CustomersController : ApiController
{
  [Authorize(Users="Peter")]
  public Customer Get()
  {

A primeira etapa da criação de um módulo HTTP de autorização personalizado é adicionar uma classe ao seu projeto de serviço que implemente as interfaces IHttpModule e IDisposable. No método Init da classe, você deverá conectar dois eventos do objeto HttpApplication transmitido para o método. O método que você anexar ao evento AuthenticateRequest será chamado quando as credenciais do cliente forem apresentadas. No entanto, você também deve conectar o método EndRequest para gerar a mensagem que faz com que o cliente envie as credenciais para você. Você também precisará de um método Dispose, mas não é necessário colocar algo nele para oferecer suporte ao código usado aqui:

public class PHVHttpAuthentication : IHttpModule, IDisposable
{
  public void Init(HttpApplication context)
  {
    context.AuthenticateRequest += AuthenticateRequests;
    context.EndRequest += TriggerCredentials;
  }
  public void Dispose()
  {
  }

Um HttpClient enviará as credenciais em resposta a um cabeçalho WWW-­Authenticate que você incluiu na resposta HTTP. Você deve incluir esse cabeçalho quando uma solicitação gerar um código de status 401 (o ASP.NET irá gerar um código de resposta 401 quando o acesso do cliente a um serviço protegido for negado). O cabeçalho deve fornecer uma dica sobre o método de autenticação em uso e o realm no qual a autenticação será aplicada (o realm pode ser qualquer cadeia de caracteres arbitrária e é usado para sinalizar para o navegador diferentes áreas do servidor). O código para enviar essa mensagem é o que você coloca no método conectado ao evento EndRequest. Este exemplo gera uma mensagem que especifica a autenticação Básica está sendo usada no realm PHVIS:

private static void TriggerCredentials(object sender, EventArgs e)
{
  HttpResponse resp = HttpContext.Current.Response;
  if (resp.StatusCode == 401)
  {
    resp.Headers.Add("WWW-Authenticate", @"Basic realm='PHVIS'");
  }
}

No método que você conectou ao método AuthenticateRequest, você deverá recuperar os cabeçalhos Authorization que o cliente enviará como resultado de receber sua mensagem 401/WWW-Authenticate:

private static void AuthenticateRequests(object sender,
  EventArgs e)
{
  string authHeader =     
    HttpContext.Current. Request.Headers["Authorization"];
  if (authHeader != null)
  {

Depois que você tiver determinado que o cliente transmitiu os elementos do cabeçalho Authorization (e continuando com minha pressuposição anterior que o site está usando a autenticação Básica), você deve analisar os dados que contêm nome de usuário e senha. O nome de usuário e a senha são codificados na Base64 e separados por dois-pontos. Este código recupera o nome de usuário e a senha em uma matriz de cadeia de caracteres de duas posições:

AuthenticationHeaderValue authHeaderVal =
  AuthenticationHeaderValue.Parse(authHeader);
if (authHeaderVal.Parameter != null)
{
  byte[] unencoded = Convert.FromBase64String(
    authHeaderVal.Parameter);
  string userpw =
    Encoding.GetEncoding("iso-8859-1").GetString(unencoded);
  string[] creds = userpw.Split(':');

Como esse código demonstra, nomes de usuários e senhas são enviados com texto não criptografado. Se você não ativar o SSL, os nomes de usuários e as senhas poderão ser capturados facilmente (e esse código funciona mesmo que o SSL esteja ativado).

A próxima etapa é validar o nome de usuário e a senha usando qualquer mecanismo apropriado para você. Independentemente de como você validar a solicitação (é provável que o código que uso no exemplo a seguir seja muito simples), a etapa final é criar uma identidade para o usuário que será usada nos processos de autorização mais tarde no pipeline do ASP.NET.

Para transmitir essas informações de identidade pelo pipeline, você cria um objeto GenericIdentity com o nome da identidade que deseja atribuir ao usuário (no código a seguir, presumi que a identidade é o nome de usuário enviado no cabeçalho). Depois de criar o objeto GenericIdentity, você deve colocá-lo na propriedade CurrentPrincipal da classe Thread. O ASP.NET também mantém um segundo contexto de segurança no objeto HttpContext e, se o seu host for o IIS, você deve oferecer suporte a ele configurando uma propriedade User na propriedade Current de HttpContext para seu objeto GenericIdentity:

if (creds[0] == "Peter" && creds[1] == "pw")
{
  GenericIdentity gi = new GenericIdentity(creds[0]);
  Thread.CurrentPrincipal = new GenericPrincipal(gi, null);
  HttpContext.Current.User = Thread.CurrentPrincipal;
}

Caso deseje oferecer suporte à segurança baseada em funções, você deverá transmitir uma matriz de nomes de funções como o segundo parâmetro para o construtor GenericPrincipal. Este exemplo atribui todos os usuários para as funções de gerente e administrador:

string[] roles = "manager,admin".Split(',');
Thread.CurrentPrincipal = new GenericPrincipal(gi, roles);

Para integrar seu módulo HTTP ao processamento de seu site, no arquivo web.config de seu projeto, use a opção de adicionar marca no elemento dos módulos: O atributo de tipo da opção de adicionar marca deve ser definido como uma cadeia de caracteres que consiste no nome de classe totalmente qualificado seguido do nome do assembly de seu módulo:

<modules>
  <add name="myCustomerAuth"
    type="SecureWebAPI.PHVHttpAuthentication, SecureWebAPI"/>
</modules>

O objeto GenericIdentity que você criou trabalhará com o atributo Authorize do ASP.NET. Você também pode acessar o GenericIdentity em um método de serviço para realizar atividades de autorização. É possível, por exemplo, fornecer serviços diferentes para usuários conectados e anônimos ao determinar se um usuário foi autenticado, marcando a propriedade IsAuthenticated do objeto GenericIdentity (IsAuthenticated retorna falso para o usuário anônimo):

if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
{

Você pode recuperar o objeto GenericIdentity de uma forma mais simples por meio da propriedade User:

if (User.Identity.IsAuthenticated)
{

Criando um cliente compatível

Para consumir os serviços protegidos por esse módulo, um cliente que não seja ­JavaScript deve fornecer um nome de usuário e senha aceitáveis. Para fornecer essas credenciais usando o HttpClient do .NET, primeiro você deve criar um objeto HttpClientHandler e definir sua propriedade Credentials como um objeto NetworkCredential que contém o nome de usuário e a senha (ou definir a propriedade UseDefaultCredentials do objeto HttpClientHandler como verdadeiro para usar as credenciais do Windows atuais do usuário). Em seguida, você cria seu objeto HttpClient, transmitindo o objeto HttpClientHandler:

HttpClientHandler hch = new HttpClientHandler();
hch.Credentials = new NetworkCredential ("Peter", "pw");
HttpClient hc = new HttpClient(hch);

Com essa configuração concluída, você pode emitir sua solicitação para o serviço. O HttpClient só apresentará as credenciais quando o acesso ao serviço for negado e ele receber a mensagem WWW-Authenticate. Se as credenciais fornecidas pelo HttpClient não forem aceitáveis, o serviço retornará um HttpResponseMessage com o StatusCode de seu Result definido como “não autenticado”.

O código a seguir chama um serviço usando o método GetAsync, verifica se há um resultado bem-sucedido e (se não obter um) exibe o código de status retornado do serviço:

hc.GetAsync("http://phvis.com/api/Customers").ContinueWith(r =>
{
  HttpResponseMessage hrm = r.Result;
  if (hrm.IsSuccessStatusCode)
  {
    // ... Process response ...
  }
  else
  {
    MessageBox.Show(hrm.StatusCode.ToString());
  }
});

Presumindo que você ignore o processo de logon do ASP.NET para clientes que não sejam ­JavaScript, como fiz aqui, nenhum cookie de autenticação será criado e cada solicitação do cliente será validada individualmente. Para reduzir a sobrecarga de validar repetidamente as credenciais fornecidas pelo cliente, você deve considerar a possibilidade de armazenar em cache as credenciais que você recupera no serviço (e usar seu método Dispose para descartar essas credenciais armazenadas em cache).

Trabalhando com certificados do cliente

Em um módulo HTTP, você recupera um objeto de certificado do cliente (e garante que ele está presente e é válido) com um código como este:

System.Web.HttpClientCertificate cert =
  HttpContext.Current.Request.ClientCertificate;
if (cert!= null && cert.IsPresent && cert.IsValid)
{

Um pouco depois no pipeline de processamento — em um método de serviço, por exemplo — você recupera o objeto do certificado (e verifica se existe algum) com este código:

X509Certificate2 cert = Request.GetClientCertificate();
if (cert!= null)
{

Se um certificado estiver presente e for válido, você também pode verificar valores específicos nas propriedades do certificado (por exemplo, entidade ou emissor).

Para enviar certificados com um HttpClient, a primeira etapa é criar um objeto WebRequestHandler em vez de um objeto HttpClientHandler (WebRequestHandler oferece mais opções de configuração do que HttpClientHandler):

WebRequestHandler wrh = new WebRequestHandler();

Para que o HttpClient pesquise automaticamente os repositórios de certificados do cliente, você deve configurar ClientCertificateOptions do objeto WebRequestHandler como o valor Automatic na enum ClientCertificateOption:

wrh.ClientCertificateOptions = ClientCertificateOption.Manual;

Por padrão, porém, o cliente deve anexar explicitamente os certificados ao WebRequestHandler no código. Você pode recuperar o certificado de um dos repositórios de certificados do cliente como faz este exemplo, que recupera um certificado do repositório CurrentUser usando o nome do emissor:

X509Store certStore;
X509Certificate x509cert;
certStore = new X509Store(StoreName.My, 
  StoreLocation.CurrentUser);  
certStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
x509cert = certStore.Certificates.Find(
  X509FindType.FindByIssuerName, "PHVIS", true)[0];
store.Close();

Se o usuário tiver recebido um certificado do cliente que, por algum motivo, não será adicionado ao repositório de certificados do usuário, você poderá criar um objeto X509Certificate no arquivo do certificado com um código como este:

x509cert = new X509Certificate2(@"C:\PHVIS.pfx");

Independentemente de como o X509Certificate for criado, as etapas finais no cliente são adicionar o certificado à coleção ClientCertificates do WebRequestHandler e, em seguida, usar o Web­RequestHandler configurado para criar o HttpClient:

wrh.ClientCertificates.Add(x509cert);
hc = new HttpClient(wrh);

Autorizando em um ambiente de hospedagem interna

Embora você não possa usar um HttpModule em um ambiente de hospedagem interna, o processo para proteger as solicitações no início do pipeline de processamento de um serviço com hospedagem interna é o mesmo: obter as credenciais da solicitação, usar essas informações para autenticar a solicitação e criar uma identidade para ser transmitida à propriedade CurrentPrincipal do thread atual. O mecanismo mais simples é criar um validador de nome de usuário e senha. Para fazer mais do que apenas validar uma combinação de nome de usuário e senha, você pode criar um manipulador de delegação. Primeiro, abordarei como integrar um validador de nome de usuário e senha.

Para criar um validador (ainda presumindo que você está usando a autenticação Básica), você deve criar uma classe que herda de User­NamePasswordValidator (é preciso adicionar uma referência à biblioteca System.IdentityModel ao seu projeto). O único método da classe base que você precisa substituir é o método Validate, que receberá o nome de usuário e a senha enviados ao serviço pelo cliente. Assim como antes, assim que validar o nome de usuário e a senha, você deve criar um objeto GenericPrincipal e usá-lo para definir a propriedade CurrentPrincipal na classe Thread (como você não está usando o IIS como seu host, não é preciso definir a propriedade HttpContext User):

public class PHVValidator :
  System.IdentityModel.Selectors.UserNamePasswordValidator
{
  public override void Validate(string userName, string password)
  {
    if (userName == "Peter" && password == "pw")
    {
      GenericIdentity gi = new GenericIdentity(username, null);
      Thread.CurrentPrincipal = gi;
    }

O código a seguir cria um host para um controlador chamado Customers com um ponto de extremidade de http://phvis.com/MyServices e especifica um novo validador:

partial class PHVService : ServiceBase
{
  private HttpSelfHostServer shs;
  protected override void OnStart(string[] args)
  {
    HttpSelfHostConfiguration hcfg =
      new HttpSelfHostConfiguration("http://phvis.com/MyServices");
    hcfg.Routes.MapHttpRoute("CustomerServiceRoute",
      "Customers", new { controller = "Customers" });
    hcfg.UserNamePasswordValidator = new PHVValidator;       
    shs = new HttpSelfHostServer(hcfg);
    shs.OpenAsync();

Manipuladores de mensagens

Para fazer mais do que validar o nome de usuário e a senha, você pode criar um manipulador de mensagens personalizado da API Web. Os manipuladores de mensagens têm vários benefícios em comparação a um módulo HTTP: os manipuladores de mensagens não são vinculados ao IIS, por isso, a segurança aplicada em um manipulador de mensagens funcionará com qualquer host; os manipuladores de mensagens são usados apenas pela API Web, por isso, fornecem uma forma simples de realizar a autorização (e atribuir identidades) para seus serviços usando um processo diferente daquele usado com suas páginas de site; e você também pode atribuir manipuladores de mensagens para rotas específicas, de modo que seu código de segurança seja chamado apenas onde for necessário.

A primeira etapa da criação de um manipulador de mensagens é escrever uma classe que herda de DelegatingHandler e substituir seu método SendAysnc:

public class PHVAuthorizingMessageHandler: DelegatingHandler
{
  protected override System.Threading.Tasks.Task<HttpResponseMessage>
    SendAsync(HttpRequestMessage request,
      System.Threading.CancellationToken cancellationToken)
  {

Nesse método (e pressupondo que você esteja criando um manipulador por rota), você pode definir a propriedade InnerHandler de DelegatingHandler para que esse manipulador possa ser vinculado no pipeline com outros manipuladores:

HttpConfiguration hcon = request.GetConfiguration();
InnerHandler = new HttpControllerDispatcher(hcon);

Para esse exemplo, irei presumir que uma solicitação válida deve ter um token simples em sua cadeia de caracteres de consulta (bem simples: um par nome/valor de “authToken=xyx”). Se o token estiver ausente ou não estiver definido como xyx, o código retornará um código de status 403 (Proibido).

Primeiro, transformo a cadeia de caracteres de consulta em um conjunto de pares nome/valor chamando o método GetQueryNameValuePairs no objeto HttpRequestMessage transmitido ao método. Em seguida, uso LINQ para recuperar o token (ou nulo se o token estiver ausente). Se o token estiver ausente ou for inválido, crio um HttpResponseMessage com o código de status HTTP adequado, encapsulo-o em um objeto TaskCompletionSource e retorno-o:

string usingRegion = (from kvp in request.GetQueryNameValuePairs()
                      where kvp.Key == "authToken"
                      select kvp.Value).FirstOrDefault();
if (usingRegion == null || usingRegion != "xyx")
{
  HttpResponseMessage resp =
     new HttpResponseMessage(HttpStatusCode.Forbidden);
  TaskCompletionSource tsc =
     new TaskCompletionSource<HttpResponseMessage>();
  tsc.SetResult(resp);
  return tsc.Task;
}

Se o token estiver presente e definido com o valor certo, crio um objeto Generic­Principal e o uso para definir a propriedade CurrentPrincipal de Thread (para oferecer suporte ao uso desse manipulador de mensagens no IIS, também defino a propriedade HttpContext User se o objeto HttpContext não for nulo):

Thread.CurrentPrincipal = new GenericPrincipal(
  Thread.CurrentPrincipal.Identity.Name, null);     
if (HttpContext.Current != null)
{
  HttpContext.Current.User = Thread.CurrentPrincipal;
}

Com a solicitação autenticada pelo token e a identidade definida, o manipulador de mensagens chama o método base para continuar o processamento:

return base.SendAsync(request, cancellationToken);

Se o seu manipulador de mensagens for ser usado em todos os controladores, você pode adicioná-lo ao pipeline de processamento da API Web como qualquer outro manipulador de mensagens. No entanto, para limitar seu manipulador para ser usado apenas em rotas específicas, você deve adicioná-lo pelo método MapHttpRoute. Primeiro, crie uma instância de sua classe e, em seguida, transmita-a como o quinto parâmetro para MapHttpRoute (este código exige uma instrução Imports/using para System.Web.Http):

routes.MapHttpRoute(
  "ServiceDefault",
  "api/Customers/{id}",
  new { id = RouteParameter.Optional },
  null,
  new PHVAuthorizingMessageHandler());

Em vez de definir InnerHandler no DelegatingHandler, você pode definir a propriedade InnerHandler como o distribuidor padrão como parte da definição de sua rota:

routes.MapHttpRoute(
  "ServiceDefault",
  "api/{controller}/{id}",
  new { id = RouteParameter.Optional },
  null,
  new PHVAuthorizingMessageHandler
  {InnerHandler = new HttpControllerDispatcher(
    GlobalConfiguration.Configuration)});

Agora, em vez de distribuir sua configuração de InnerHandler entre vários DelegatingHandlers, você o gerencia em um único local, onde você define suas rotas.

Estendendo a entidade de segurança

Se a autorização de solicitações por nome e função não for suficiente, você pode estender o processo de autorização ao criar sua própria classe de entidade de segurança, implementando a interface IPrincipal. No entanto, para utilizar uma classe de entidade de segurança personalizada, você deverá criar seu próprio atributo de autorização personalizado ou adicionar um código personalizado aos seus métodos de serviço.

Por exemplo, se você tiver um conjunto de serviços que pode ser acessado apenas por usuários de uma região específica, pode criar uma classe de entidade de segurança simples que implementa a interface IPrincipal e adiciona uma propriedade Region, como mostra a Figura 2.

Figura 2 Criando uma entidade de segurança personalizada com propriedades adicionais

public class PHVPrincipal: IPrincipal
{
  public PHVPrincipal(string Name, string Region)
  {
    this.Name = Name;
    this.Region = Region;
  }
  public string Name { get; set; }
  public string Region { get; set; }
  public IIdentity Identity
  {
    get
    {
      return new GenericIdentity(this.Name);
    }
    set
    {
      this.Name = value.Name;
    }
   }
   public bool IsInRole(string role)
   {
     return true;
   }

Para utilizar essa nova classe de entidade de segurança (que funcionará com qualquer host), você deve apenas criar uma instância dela e, em seguida, usá-la para definir as propriedades CurrentPrincipal e User. O código a seguir procura um valor na cadeia de caracteres de consulta da solicitação associada com o nome “região”. Depois de recuperar esse valor, o código o utiliza para definir a propriedade Region da entidade de segurança, transmitindo o valor para o construtor da classe:

string region = (from kvp in request.GetQueryNameValuePairs()
                 where kvp.Key == "region"
                 select kvp.Value).FirstOrDefault();
Thread.CurrentPrincipal = new PHVPrincipal(userName, region);

Se estiver trabalhando no Microsoft .NET Framework 4.5, em vez de implementar a interface IPrincipal, você deve herdar da nova classe ClaimsPrincipal. A ClaimsPrincipal oferece suporte ao processamento baseado em declarações e à integração com o Windows Identity Foundation (WIF). Isso, no entanto, está fora do escopo deste artigo (abordarei esse tópico em um artigo futuro sobre segurança com base em declarações).

Autorizando uma entidade de segurança personalizada

Com um novo objeto de entidade de segurança em vigor, você pode criar um atributo de autorização que utiliza os novos dados transportados pela entidade de segurança. Primeiro, crie uma classe que herde de System.Web.Http.AuthorizeAttri­bute e substitua seu método IsAuthorized (esse processo é diferente da prática do ASP.NET MVC, onde você cria atributos Authorization estendendo System.Web.Http.Filters.Autho­rizationFilterAttribute). O método IsAuthorized recebe um HttpActionContext, cujas propriedades podem ser usadas como parte de seu processo de autorização. No entanto, esse exemplo precisa apenas extrair o objeto da entidade de segurança da propriedade CurrentPrincipal de Thread, convertê-lo no tipo de entidade de segurança personalizada e marcar a propriedade Region. Se a autorização obtiver êxito, o código retornará verdadeiro. Se a autorização falhar, você precisará da propriedade ActionContext para criar uma resposta personalizada antes de retornar falso, como mostra a Figura 3.

Figura 3 Filtrando um objeto da entidade de segurança personalizada

public class RegionAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
  public string Region { get; set; }
  protected override bool IsAuthorized(HttpActionContext actionContext)
  {
    PHVPrincipal phvPcp = Thread.CurrentPrincipal as PHVPrincipal;
    if (phvPcp != null && phvPcp.Region == this.Region)
    {
      return true;
    }
    else
    {
      actionContext.Response =
        new HttpResponseMessage(
          System.Net.HttpStatusCode.Unauthorized)
        {
          ReasonPhrase = "Invalid region"
        };
      return false;
    }        
  }
}

Seu filtro de autorização personalizado pode ser usado como o filtro Authorize padrão do ASP.NET. Como esse filtro tem uma propriedade Region, essa propriedade deve ser definida para a região aceitável para esse método como parte da decoração de um método de serviço:

[RegionAuthorize(Region = "East")]
public HttpResponseMessage Get()
{

Para esse exemplo, escolhi herdar de Authorize­Attribute, pois meu código de autorização é totalmente associado à CPU. Se meu código precisasse acessar algum recurso de rede (ou executar alguma E/S), uma opção melhor seria implementar a interface IAuthorization­Filter, pois ela oferece suporte à realização de chamadas assíncronas.

Como disse no início deste artigo: o cenário típico da API Web não exige autorização adicional, exceto para proteger contra ataques CSFR. No entanto, quando você precisa estender o sistema de segurança padrão, a API Web fornece várias opções em todo o pipeline de processamento, no qual você pode integrar qualquer proteção necessária. E é sempre melhor ter opções.

Peter Vogel é diretor da PH&V Information Services, especialista em desenvolvimento de ASP.NET com experiência em arquitetura orientada a serviços, XML, banco de dados e design de interface do usuário.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Dominick Baier (thinktecture GmbH & Co KG), Barry Dorrans (Microsoft) e Mike Wasson (Microsoft)
Mike Wasson (mwasson@microsoft.com) trabalha como programador e redator na Microsoft. Atualmente, escreve sobre ASP.NET, com foco em API Web.

Barry Dorrans (Barry.Dorrans@microsoft.com) é desenvolvedor de segurança na Microsoft, trabalhando com a equipe da plataforma Windows Azure. Escreveu “Beginning ASP.NET Security” e era MVP de segurança de desenvolvedor antes de entrar na Microsoft. Apesar disso, ainda erra a grafia de criptografia com frequência.

Dominick (dominick.baier@thinktecture.com) é consultor de segurança da thinktecture (thinktecture.com). Seu foco principal é controle de acesso e identidades em aplicativos distribuídos, além de ser o criador dos projetos populares de código aberto IdentityModel e IdentityServer. Você pode visitar seu blog em leastprivilege.com.