Desenvolvimento - WCF

Autenticação com WCF e jQuery

Apesar do WCF fornecer várias alternativas para isso, quando estamos em um ambiente REST, alguns cuidados especiais são necessários, quais serão abordados no decorrer deste artigo.

por Israel Aéce



Recentemente escrevi sobre o consumo de serviços WCF a partir do jQuery. Naquele artigo foi comentado como construir serviços WCF para ser acessados através do ambiente REST, e além disso, vimos também como proceder para o consumo deste serviço utilizando o jQuery como cliente.

Mas um detalhe que não foi abordado no artigo foi a questão da segurança, ou melhor, do processo de autenticação do cliente que consome o serviço. Por exemplo, imagine que temos um serviço que exige que o usuário se identifique, para que assim possamos determinar se ele terá ou não acesso ao serviço. Apesar do WCF fornecer várias alternativas para isso, quando estamos em um ambiente REST, alguns cuidados especiais são necessários, quais serão abordados no decorrer deste artigo.

O primeiro detalhe para nos atentarmos é como vamos configurar o serviço para que o mesmo possa exigir as credenciais. Para expor um serviço para ser consumido através do ambiente REST, utilizamos um binding exclusivo chamado de WebHttpBinding. Em sua configuração padrão, não há nenhum tipo de autenticação configurada, mas como qualquer binding, podemos recorrer à opção security para isso.

Temos apenas três opções relacionadas à segurança para este binding: None, Transport e TransportCredentialOnly. A primeira opção desabilita qualquer tipo de proteção e autenticação no respectivo serviço. Já a segunda opção, Transport, determina que será o protocolo que deverá garantir a segurança da mensagem (HTTPS) e, finalmente, o TransportCredentialOnly, que não fornecerá integridade e confidencialidade na mensagem que está sendo trafegada, e utilizará apenas o protocolo HTTP exclusivamente para autenticação do usuário.

Depois desta configuração que determina como a comunicação deverá ser protegida, temos uma espécie de "sub-configuração" dela que precisamos nos atentar. Esta sub-configuração determina como as credenciais serão passadas para o serviço. Entre as várias formas, temoso HTTPBasic, que faz com que as credenciais viagem do cliente para o serviço sem qualquer espécie de criptografia. Esse modelo, em sua configuração padrão, tem uma forte afinidade com o Windows/Active Directory, ou seja, para utilizar esse recurso, as credenciais informadas do lado do cliente deverão refletir uma conta válida no Windows/Active Directory, algo que não vamos nos preocupar neste momento. A configuração do serviço WCF deverá ficar da seguinte forma:

<system.serviceModel>
<services>
<service
name="RESTComBasicAuthentication.Services.ServicoDeUsuarios"
behaviorConfiguration="config">
<endpoint
address=""
binding="webHttpBinding"
contract="RESTComBasicAuthentication.Services.IUsuarios"
behaviorConfiguration="edpConfig"
bindingConfiguration="bc" />
</service>
</services>
<bindings>
<webHttpBinding>
<binding name="bc">
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Basic" />
</security>
</binding>
</webHttpBinding>
</bindings>
<!-- Outras Configurações -->
</system.serviceModel>

Quando utilizamos o modelo Basic, ao tentar acessar um recurso protegido, o próprio browser é capaz de identificar e abrir uma janela com os campos para informarmos o username e password, só que não podemos nos esquecer de que esse serviço será consumido por algum cliente, e que o foco aqui é o uso do jQuery, e com isso, de alguma forma precisamos enviar o username e password do cliente para o serviço. Como já sabemos, o jQuery oferece uma função chamada $.ajax, que nos permite configurar e efetuar a chamada para um determinado serviço ou algum outro método. Internamente, este método recorrea umobjeto popularmente conhecido, que é oXMLHttpRequest. Este objeto é o responsável porgerenciar toda a comunicação entre o cliente e o serviço.

De acordo com a especificação deste objeto, há um método chamado Open, que entre os parâmetros convencionais, como endereço até o serviço, o tipo da requisição (Json ou Xml), há também como informar mais duas informações relacionadas ao processo de autenticação: username e password. Através dos parâmetros username e password, definimos duas strings que representam cada uma dessas informações. Como o jQuery abstrai o acesso a este objeto, a função $.ajax também nos permite informar o usuário e senha, como já mencionado acima. Sabendo da existência destes parâmetros, podemos configurar a chamada para o serviço através do jQuery da seguinte forma:

function RecuperarUsuario() {
$.ajax(
{
type: "POST",
username: "UsuarioQueExisteNoWindows",
password: "123456",
url: "http://localhost/Services/ServicoDeUsuarios.svc/RecuperarUsuario",
contentType: "application/json",
data: "{ "nome": "Israel Aece", "email": "ia@israelaece.com" }",
processData: true,
success:
function (resultado) {
alert(resultado.RecuperarUsuarioResult.Nome);
alert(resultado.RecuperarUsuarioResult.Email);
},
});
}

Se você monitorar a requisição para este serviço com alguma ferramenta, como é o caso do Fiddler, você verá que o username e password não serão enviados até que realmente seja necessário. O modelode autenticaçãoBasic é baseado no processo conhecido como "Desafio HTTP 401", que funciona da seguinte forma:

  • 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 (Access Denied), que instrui o cliente (browser) a solicitar as credenciais de acesso.
  • Uma vez informado, o browser recria a mesma requisição, mas agora envia nos headers da mesma ousername epassword 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.

Apesar das credenciais estarem em hard-code no exemplo acima, nada impede de criarmos uma tela onde o cliente pode digitar um usuário e senha, mas lembrando que este usuário deverá exisitir no Windows/Active Directory.

Customização

O principal ponto negativo da solução acima é a necessidade de termos os usuários cadastrados no Windows, algo que pode ser inviável em um ambiente que não é controlado, como é o caso da internet. Algo muito comum em aplicações deste tipo, é a validação do usuário em uma base de dados, ou até mesmo, utilizando a estrutura do Membership do ASP.NET. Para atingirmos esse objetivo, precisamos entender um pouco mais sobre o processo de autenticação e também alguns pontos de estensibilidade.

Quando configuramos o modelo Basic, o IIS é o responsável por coordenar todo o processo de acesso ao recurso, e se o usuário não estiver autenticado, o próprio IIS devolve a mensagem com o código 401 e o header específico para o mesmo (1), para que o browser proceda com a solicitação das credenciais (2), e depois de informadas, refaz a requisição embutindo o header com as credencias codificadas (3). A imagem abaixo ilustra esse processo:



No exemplo que vimos acima, o único recurso protegido pelo modelo Basic é apenas o arquivo ServicoDeUsuarios.svc. Agora, se desejamos customizar, temos que trazer todo o controle do processo de autenticação para a aplicação, ou seja, não iremos mais querer que o IIS coordene o processo, que como sabemos, ele recorre ao Windows para validar os usuários. Sendo assim, precisamos permitir o acesso anônimo ao arquivo *.svc.

Há algum tempo eu comentei sobre a possibilidade de efetuar a customização da autenticação do WCF através de usuário e senha, utilizando a classe UserNamePasswordValidator para essa validação. O problema é que o binding WebHttpBinding não suporta a customização deste validador, o que nos obrigariaà descer o nível até o pipeline do ASP.NET para implementar algo específico para o WCF.

Ao invés disso, podemos recorrer aos interceptadores (Interceptors). Os interceptadores não estão nativamente dentro do WCF, mas estão disponíveis ao instalar o WCF-REST Starter Kit. Este kit nada mais é que um conjunto de funcionalidades que podemos incorporar aos nossos projetos WCF baseados em REST, tornando algumas tarefas comuns em algo mais simples de se resolver/implementar, como vai ser o caso aqui.

Ao instalar este kit, teremos à nossa disposição alguns novos assemblies, e entre eles um chamado Microsoft.ServiceModel.Web.dll. Uma das classes fornecidas por ele é a classe abstrata RequestInterceptor. Essa classe fornece um método abstrato chamado ProcessRequest, que permite interceptar a requisição, antes mesmo dela começar a ser executada, fornecendo um parâmetro do tipo RequestContext, que representa o contexto da requisição atual, qual será utilizado para a interação com o cliente que está tentando acessar o recurso.

Com esta possibilidade, podemos criar um interceptador que extrai o header da requisição 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 do modelo HTTP Basic, para conseguirmos dialogar com o cliente, para que assim ele consiga coordenar o processo de autenticação do usuário.

Como havia dito acima, o username e password 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 username e password.

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. O código abaixo ilustra esse processo:

string credentials = mp.Headers["Authorization"];

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

int separator = decodedCredentials.IndexOf(":");
username = decodedCredentials.Substring(0, separator);
password = decodedCredentials.Substring(separator + 1);
}

Caso o header não exista ou o usuário não existir na base de dados, então podemos criar o header para que o cliente seja - novamente - obrigado a informar as credenciais. Para isso, utilizamos as propriedades da mensagem, e configuramos a mensagem de retorno. A mensagem é representada no WCF pela classe Message. O código abaixo ilustra a criação, configuração e resposta ao cliente com esta mensagem recém criada:

private void GenerateAccessDeniedMessage(ref RequestContext requestContext)
{
Message reply = Message.CreateMessage(MessageVersion.None, null, null,
new DataContractJsonSerializer(typeof(object)));

HttpResponseMessageProperty hrp = new HttpResponseMessageProperty();
hrp.StatusCode = HttpStatusCode.Unauthorized;
hrp.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", this.Realm));
reply.Properties[HttpResponseMessageProperty.Name] = hrp;

requestContext.Reply(reply);
requestContext = null;
}

Se o usuário for válido, então é necessário inicializar o contexto de segurança do WCF, que é representado pela classe ServiceSecurityContext, que utiliza uma instância da classe GenericPrincipal para identificar o usuário logado.

Finalmente, tudo isso não funciona por si só. Ainda precisamos acoplar a instância desta classe à execução. Para isso, devemos recorrer a customização da classe que cria o host, que é responsável por gerenciar a execução da classe que representa o serviço. A classe ServiceHostFactory fornece um método chamado CreateServiceHost, que como o próprio nome diz, permite retornar qualquer classe que herde direta ou indiretamente da classe ServiceHost. Dentro do assembly do WCF-Rest Starter Kit, temos uma versão específica de host, chamada de WebServiceHost2. A grande diferença deste host, é que ele expõe uma propriedade chamada Interceptors, que permite adicionarmos qualquer interceptador, desde que ele herde da classe RequestInterceptor, como já vimos acima. Depois que a factory customizada foi criada, devemos alterar o arquivo *.svc para que ela seja utilizada:

<%@ ServiceHost
Language="C#"
Debug="true"
Factory="RESTComBasicAuthentication.CustomRestHostFactory"
Service="RESTComBasicAuthentication.Services.ServicoDeUsuarios"
CodeBehind="ServicoDeUsuarios.svc.cs" %>

Uma vez que acoplamos este interceptador, ele é quem gerenciará o processo de autenticação, ou seja, o cliente irá dialogar com este interceptador, mas para que isso funcione corretamente, é necessário que a autenticação Basic esteja desabilitada no IIS. Através da imagem abaixo, podemos reparar este novo comportamento:

Conclusão: Apesar de não temos nativamente no WCF uma forma de customizar a autenticação em serviços REST, podemos recorrer ao WCF-REST Starter Kit para conseguir efetuar essa customização, utilizando a infraestrutura oferecida pelo modeloHTTP Basic para enviar as credenciais. Apesar de tudo funcionar como deveria, é importante dizer que os dados continuam sendo trafegados sem qualquer proteção, e para evitar que alguém visualize o conteúdo, é importante que toda a comunicação seja protegida pelo HTTPS.

RESTComBasicAuthentication.zip (328.00 kb)

Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.