Quando criamos algum tipo de serviço, em princípio nos concentramos nas funcionalidades e informações que ele irá expor aos consumidores. Depois disso, nos preocuparemos em analisar o contexto de segurança, verificando o tipo de autenticação e nível de autorização que são necessários para o serviço funcionar de forma segura.
O detalhe é que muitas vezes não queremos envolver qualquer mecanismo de autenticação e autorização no serviço, já que ele poderá ser acessado de forma pública, sem qualquer necessidade do consumidor efetivamente se cadastrar, gerar um login e senha e, finalmente, acessar o respectivo serviço, apresentando à ele essas informações. Mas apesar do cliente não se identificar com um login e senha, não quer dizer que nosso serviço não precise ser “protegido” de alguma outra forma.
A questão é que permitindo o acesso irrestrito total, permite com que bons usuários o acessem da forma legal, e também permite com que outros usuários, estes mal intencionados, disparem uma infinidade de requisições contra o serviço, e se este, por sua vez, pode estar consumindo recursos caros, com por exemplo, acessando uma base de dados, em pouco tempo nosso servidor estará esgotado, não conseguindo atender nem mesmo aqueles que usam o serviço da forma correta.
Uma técnica chamada API Key Verification foi criada com o intuito de evitar esse tipo de problema. Com uma implementação simples, criamos e mantemos uma lista de chaves (podendo ser strings, inteiros, Guids, etc.) que estão habilitadas à acessarem o serviço. Os consumidores podem receber esta chave de alguma forma (out-of-band ou não), como por exemplo, fazendo um cadastro no site da empresa demonstrando o interesse; mais tarde, ele receberá um e-mail contendo a chave que foi criada exclusivamente para o mesmo, e que deverá ser utilizada quando efetuar uma requisição para o serviço, embutindo-a na coleção de headers ou querystrings, de acordo com o que você determinar. Ao chegar a requisição no serviço, você detecta a presença desta informação, e se a chave não fizer parte da requisição ou for uma chave inválida, deveremos rejeitar a mesma.
A ideia deste artigo é mostrar como podemos implementar esta validação utilizando a nova API do WCF, chamada WCF Web API. Para isso, vamos recorrer à um recurso exposto por ela, chamado de Message Handlers. Esses handlers são acoplados ao runtime, e são executados durante os primeiros estágios da requisição, antes mesmo do serviço ser efetivamente executado. Para maiores detalhes sobre os Message Handlers, consulte este artigo. Abaixo temos a implementação deste handler que validará a chave que o consumidor está nos encaminhando:
public class ApiKeyVerificationMessageHandler : DelegatingChannel
{
private readonly IRepositorioDeChaves repositorioDeChaves;
private const string API_KEY = “apiKey”;
public ApiKeyVerificationMessageHandler(HttpMessageChannel inner, IRepositorioDeChaves repositorio)
: base(inner)
{
this.repositorioDeChaves = repositorio;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellation)
{
var queryStrings = HttpUtility.ParseQueryString(request.RequestUri.Query);
var chave = Guid.Empty;
if (Guid.TryParse(queryStrings[API_KEY], out chave) && repositorioDeChaves.VerificarSeExiste(chave))
return base.SendAsync(request, cancellation);
return Task.Factory.StartNew(() => new HttpResponseMessage(HttpStatusCode.Unauthorized, “A chave fornecida é inválida.”));
}
}
Analisando a classe acima, podemos perceber que no construtor recebemos a instância de alguma classe que implemente a interface IRepositorioDeChaves. Utilizamos a interface aqui, justamente para tornar o nosso handler independente do local onde desejamos armazenar as chaves. Para efeito de testes, optei por criar uma implementação em memória e que ocultarei aqui por questões de espaço, mas você poderá recorrer a qualquer outro meio, como XML, SQL, etc. Em seguida, sobrescrevemos o método SendAsync (exposto pela classe DelegatingChannel), onde detectamos se existe ou não uma query string chamada apiKey, e se houver, fazemos o parser da mesma através do Guid, que é o tipo de dado que estamos utilizando para determinar o tipo das chaves que dão acesso ao serviço. Não existindo a chave na query string ou ela sendo inválida, retornamos uma mensagem informando o status como 401, que indica que a requisição não foi autorizada à acessar o serviço.
Depois disso, precisamos acoplar esse handler à execução, e para isso, utilizamos o evento ApplicationStart do arquivo Global.asax, onde vamos criar a factory responsável pela criação do handler. Essa factory será responsável por instanciar a classe ApiKeyVerificationMessageHandler que criamos acima, passando o repositório de chaves em memória que também criamos e configuramos dentro deste evento. Note que estou utilizando uma sobrecarga do método SetMessageHandlerFactory, que me permite passar um delegate contendo o código de construção do objeto, mas se quiser algo mais rebuscado, poderia construir uma factory herdando da classe HttpMessageHandlerFactory.
protected void Application_Start(object sender, EventArgs e)
{
RepositorioDeGuidsEmMemoria repositorio = new RepositorioDeGuidsEmMemoria();
repositorio.Adicionar(new Guid(“d4907f3c-52c7-457e-9c18-e5abf9be77fe”));
repositorio.Adicionar(new Guid(“adc5c096-fdc3-4c57-a887-ba4a368f299f”));
var config =
HttpHostConfiguration
.Create()
.SetMessageHandlerFactory(inner => new ApiKeyVerificationMessageHandler(inner, repositorio));
RouteTable.Routes.MapServiceRoute<ServicoDeTeste>(“teste”, config);
}
Depois dessas customizações, podemos utilizar o Fiddler para efetuar e capturar as requisições para o serviço. Para exemplificar, a primeira requisição informa um elemento chamado apiKey na coleção de query strings. Isso fará com que o serviço entregue a resposta da forma correta. Já na segunda requisição, omitimos a chave, fazendo com que o a resposta seja definida como 401, como também podemos perceber abaixo:
— Primeira Requisição
GET http://localhost:1830/teste/ping?apiKey=d4907f3c-52c7-457e-9c18-e5abf9be77fe&value=sss HTTP/1.1
User-Agent: Fiddler
Host: localhost:1830
— Resposta
HTTP/1.1 200 OK
Content-Length: 63
Content-Type: application/xml; charset=utf-8
<?xml version=”1.0″ encoding=”utf-8″?><string>sss ping</string>
— Segunda Requisição
GET http://localhost:1830/teste/ping?value=sss HTTP/1.1
User-Agent: Fiddler
Host: localhost:1830
— Resposta
HTTP/1.1 401 Unauthorized
Content-Length: 0
Connection: Close
Além do que foi apresentado aqui, você ainda poderia criar políticas para renovação e revogação de chaves, e além disso, permitir com que o consumidor mande a chave na coleção de query strings ou de headers, podendo o handler ser inteligente o bastante para ser capaz de detectar em ambos os locais, e caso não encontrado, rejeitaria do mesmo jeito.