Sessão e Operações One-way

by Israel Aece 12. August 2010 08:58

O WCF fornece várias tipos de mensagens, e entre eles, temos as operações one-way. Esse tipo de operação deve ser utilizado quando o cliente não precisa de qualquer retorno (resultado ou erro), uma espécie de fire-and-forget.

Esse tipo de mensagem pode ter um comportamento estranho quando exposto através de um binding que suporte sessões nativamente, como é o caso do NetTcpBinding. O protocolo TCP estabelece um canal duplex entre o cliente e o serviço, mantendo uma espécie de conexão ativa, onde a qualquer momento um pode tranquilamente se comunicar com o outro.

É importante dizer que mensagens one-way não são assíncronas, mas o tempo de entrega é tolerável. O maior problema, do qual falava anteriormente, refere-se ao fechamento do proxy depois da mensagem one-way disparada ao destinatário, ou seja, quando invocamos a operação one-way, tão logo já conseguimos continuar trabalhando na aplicação cliente, mas se tentarmos fechar o proxy, ficaremos bloqueados até que a operação one-way seja completamente finalizada. Mas se a ideia é disparar e esquecer, porque eu preciso esperar, mantendo o proxy aberto até finalizar a operação?

Para ter o comportamento fire-and-forget a sério, uma das alternativas que temos é habilitar as mensagens (ou sessões) confiáveis. Isso tornará a comunicação entre as partes muito mais descritiva, onde além das mensagens normais, ainda haverão uma porção de outras mensagens que são geradas pelo protocolo WS-ReliableMessaging, indicando se a mensagem foi entregue, quando ela foi entregue e a ordem de chegada, permitindo ao cliente continuar sem a necessidade de esperar. Para habilitar as mensagens confiáveis, consulte este artigo.

Apesar disso funcionar, a comunicação entre as partes ficará bem mais volumosa, já que várias mensagens de infraestrutura serão trocadas para garantir a entrega da mensagem principal. Para evitar todo este overhead causado pelo protocolo WS-ReliableMessaging, podemos recorrer a construção de um binding customizado, empilhando os vários elementos para compor a comunicação via TCP, mas com um detalhe, que é a adição de um novo elemento, chamado de OneWayBindingElement. A adição deste elemento fará com que o canal de comunicação se comporte semanticamente como um fire-and-forget. O código abaixo ilustra o seu uso, via código imperativo e declarativo, respectivamente:

private static Binding CreateOneWayTcpBinding()
{
    BindingElementCollection currentElements = new NetTcpBinding().CreateBindingElements();
    BindingElementCollection bindingElements = new BindingElementCollection();

    bindingElements.Add(new OneWayBindingElement());

    foreach (BindingElement be in currentElements)
        bindingElements.Add(be);

    return new CustomBinding(bindingElements);
}

<system.serviceModel>
  <bindings>
    <customBinding>
      <binding name="OneWayTcpBinding">
        <oneWay />
        <binaryMessageEncoding />
        <tcpTransport />
      </binding>
    </customBinding>
  </bindings>
</system.serviceModel>

Só que utilizar este procedimento possui dois detalhes importantes: o primeiro é que ao configurá-lo, o canal somente permitirá a comunicação através do modelo one-way, e com isso, o contrato que você está expondo obrigatoriamente precisa definir todas as operações neste mesmo modelo. Se tiver qualquer operação neste contrato do tipo request/reply, ao tentar subir o serviço, você irá se deparar com uma exceção do tipo InvalidOperationException, indicando justamente isso. Se você conseguir garantir com que todas as operações sejam one-way, então utilizar esta técnica pode trazer uma grande melhora em performance, quando comparado à opção anterior.

O segundo detalhe é com relação ao modelo de gerenciamento de instância. Mesmo que você diga ao serviço que ele deve ser PerSession, ele não conseguirá manter a sessão, ou seja, terá o comportamento do modelo PerCall, pois cada chamada para uma operação one-way encerrará a "sessão".

Tags: , ,

WCF

Consumindo um STS diretamente

by Israel Aece 28. July 2010 16:46

No artigo anterior eu mostrei como construir um serviço WCF que faz uso do WIF para terceirizar o processo de autenticação. Lá criamos um serviço de STS utilizando os recursos fornecidos pela própria API do WIF. Como havia dito anteriormente, este serviço especial fornecido pelo WIF, nada mais é que um serviço WCF, mas que expõe um conjunto de operações específicas para lidar com a autenticação de usuários.

Quando referenciamos o serviço em uma aplicação cliente, o arquivo de configuração já é montado com todas as entradas necessárias para que o WCF em conjunto com o WIF, façam todo o processo de autenticação, e depois disso, já podemos efetuar as chamadas para as operações que o serviço (relying party) fornece. Apesar de toda a mágica ser realizada internamente, podemos explicitamente consultar o serviço de STS, para que possamos solicitar a emissão, validação ou cancelamento de um token para um determinado usuário.

Como o serviço de STS é baseado em WCF, podemos utilizar a estrutura cliente do WCF para acessá-lo. Mas para facilitar ainda mais, a Microsoft criou uma versão específica dessa estrutura, expondo versões das mesmas classes, mas voltadas para o consumo de um serviço específico de STS. Internamente essas classes substituem o comportamento padrão, acoplando os recursos fornecidos pelo WIF. A finalidade deste artigo é mostrar o conjunto de classes que temos para efetuar essa comunicação.

A primeira classe que temos é a WSTrustChannelFactory. Essa classe é responsável por criar os famosos proxies que são utilizados pelos clientes para enviar as mensagens. Mas pelo prefixo ao nome desta classe, jé podemos perceber que ela gera toda a infraestrutura para a comunicação em cima do protocolo WS-Trust, qual já discutimos em artigos anteriores. Assim como em um serviço normal, em seu construtor precisamos informar o binding e o endereço para o serviço de STS.

Aqui entra em cena alguns novos bindings, que são utilizados exclusivamente para a comunicação com serviços de STS, e que fornecem configurações pertinentes à forma de autenticação que o usuário fará. Todos os bindings herdam da classe abstrata WSTrustBindingBase, e estão debaixo do namespace Microsoft.IdentityModel.Protocols.WSTrust.Bindings. Abaixo temos a lista com cada binding suportado e sua respectiva finalidade:

  • CertificateWSTrustBinding: Permite ao cliente apresentar um certificado para a autenticação.
  • IssuedTokenWSTrustBinding: Permite ao cliente apresentar um IssuedToken para a autenticação.
  • KerberosWSTrustBinding: Permite ao cliente apresentar um token Kerberos para a autenticação.
  • UserNameWSTrustBinding: Permite ao cliente apresentar um login e senha para a autenticação.
  • WindowsWSTrustBinding: Permite ao cliente utilizar as credenciais do Windows para a autenticação.

Depois do binding, temos que definir o certificado (chave pública) que utilizaremos. Essa chave será utilizada para proteger a mensagem de envio ao serviço de STS, garantindo que somente a chave privada correspondente consigará acessar a informação, e esta, por sua vez, estará de posse do STS. Em seguida temos a definição do nome do usuário e senha, que serão encaminhados para o serviço de STS, que serão utilizadas por ele para validar o usuário (UserNameSecurityTokenHandler).

Com a factory configurada, agora podemos recorrer ao método CreateChannel, que retornará o canal de comunicação configurado com o STS. Esse método retornará o proxy, que implementará o contrato IWSTrustChannelContract. Como podemos perceber, esse contrato disponibilizará os métodos para emissão, validação e cancelamento de tokens. O método que veremos aqui é o Issue, que recebe como parâmetro a instância da classe RequestSecurityToken (RST), que parametriza alguma ação exposta pelo contrato. A configuração desta classe exige informarmos o endereço da relying party que queremos acessar (AppliesTo), que determina se o STS pode ou não emitir o token para ela. Abaixo podemos visualizar o código que comentamos acima:

private const string STS_ADDRESS = "http://IsraelAeceNB2:9010/sts";
private const string SRV_ADDRESS = "http://IsraelAeceNB2:9000/ServicoDeCredito";

private static SecurityToken EmitirToken()
{
    using (WSTrustChannelFactory af =
        new WSTrustChannelFactory(
            new UserNameWSTrustBinding(SecurityMode.Message),
            new EndpointAddress(
                new Uri(STS_ADDRESS),
                EndpointIdentity.CreateDnsIdentity("Autenticador"))))
    {
        af.Credentials.ServiceCertificate.SetDefaultCertificate(
            "CN=Autenticador", StoreLocation.LocalMachine, StoreName.My);

        af.Credentials.UserName.UserName = "Israel";
        af.Credentials.UserName.Password = "P@ssw0rd";

        IWSTrustChannelContract channel = af.CreateChannel();
        RequestSecurityTokenResponse response = null;

        var token = channel.Issue(new RequestSecurityToken()
        {
            RequestType = RequestTypes.Issue,
            AppliesTo = new EndpointAddress(SRV_ADDRESS)
        }, out response);

        return token;
    }
}

Por último, e não menos importante, temos o retorno do método Issue. Ele retorna a instância de uma classe que herda direta ou indiretamente da classe SecurityToken, correspondendo ao token gerado pelo serviço de STS, caso a autentição tenha resultado com sucesso. É este token que informaremos para o serviço antes de efetuar a chamada para as operações. Opcionalmente você pode capturar a mensagem de resposta do processo de autenticação, e para isso, você pode utilizar uma sobrecarga que há no método Issue, que permite especificar através de um parâmetro de saída uma classe do tipo RequestSecurityTokenResponse (RSTR), e com isso ter acesso a informações como tempo de vida, tipo de autenticação, tipo do token gerado, etc.

Com o token gerado, agora nos resta passá-lo para o serviço, para que o mesmo o utilize  para permitir a execução da operação que desejarmos invocar. Para a criação do serviço, vamos também recorrer à uma factory, definindo em seu tipo genérico o contrato do serviço. Em seu construtor, passamos a instância do binding WS2007FederationHttpBinding, que permite dialogarmos com um serviço que suporta este tipo de autenticação (via STS). Além dele, ainda é necessário informarmos o endereço até o serviço, assim como já fazemos quando desejamos invocar qualquer serviço WCF. Depois deste objeto criado, precisamos configurar alguns parâmetros que são necessários para estabelecer a ligação entre o cliente e o serviço.

Essas configurações estão todas concentradas na propriedade Credentials, que é exposta pela classe ChannelFactory<TChannel>. Só que antes de analisar as propriedades que ela fornece, precisamos entender o comportamento deste binding. Como ele exige um token para enviar ao serviço, é necessário informarmos ele antes de efetuar a requisição para qualquer operação. A propriedade SupportInteractive tem a finalidade de exibir ou não a tela do Windows Cardspace, solicitando que o usuário informe este token. No cenário deste exemplo, o token já foi emitido, devido a uma solicitação explícita ao serviço de STS, e justamente por isso, devemos evitar que essa tela seja exibida, definindo-a como False.

Em seguida, especificamos a chave pública que foi fornecida pelo serviço, garantindo que a mensagem que chegará para ele venha devidamente protegida. A propriedade CertificateValidationMode se faz necessária porque precisamos desabilitar a validação do certificado, já que ele foi criado apenas para testes. Para finalizar a configuração, invocamos o método de estensão chamado ConfigureChannelFactory, que é fornecido através da classe ChannelFactoryOperations, que por sua vez, está debaixo do namespace Microsoft.IdentityModel.Protocols.WSTrust.

Essa classe ainda fornece mais um método (de estensão) importante para o nosso exemplo, que é o CreateChannelWithIssuedToken. Este método recebe como parâmetro a instância de um token, que é representado pela classe SecurityToken, que criamos no passo acima, retornando o proxy de comunicação com o serviço. É neste momento que informarmos ao serviço o token que será utilizado para efetuar a chamada para as operações dele. O código abaixo ilustra toda essa configuração, incluindo as chamadas para as operações expostas pelo serviço.

private static void InvocarServico(SecurityToken token)
{
    using (ChannelFactory<IConsultasFinanceiras> sf =
        new ChannelFactory<IConsultasFinanceiras>(
            new WS2007FederationHttpBinding(), 
            new EndpointAddress(
                new Uri(SRV_ADDRESS),
                EndpointIdentity.CreateDnsIdentity("Servico"))))
    {
        sf.Credentials.SupportInteractive = false;
        sf.Credentials.ServiceCertificate.Authentication.CertificateValidationMode =
            X509CertificateValidationMode.PeerTrust;
        sf.Credentials.ServiceCertificate.SetDefaultCertificate(
            "CN=Servico", StoreLocation.LocalMachine, StoreName.My);
        sf.ConfigureChannelFactory();

        var channel = sf.CreateChannelWithIssuedToken(token);
        channel.DefinirNovoLimiteDeCredito(10, 1000);
        Console.WriteLine(channel.RecuperarLimiteDeCredito(1000));
    }
}

Como podemos notar no exemplo acima, a chamada para o serviço está totalmente independente do modelo de  autenticação. Tudo o que precisa fazer é informar o token gerado pelo STS, sem ter ideia de como a autenticação foi realizada. Amanhã, o modelo de autenticação exigido pelo STS muda e o consumo do serviço não sofrerá qualquer alteração.

Conclusão: Neste artigo pudemos desvendar todo o procedimento que é reliazado internamente pelo WIF quando tentamos consumir um serviço WCF exige que a autenticação seja realizada por um STS. Agora coordenamos isso manualmente, o que nos dá certa flexibilidade, como por exemplo, em um ambiente passivo, evitar o redirecionamento para o STS para autenticar o usuário, onde poderíamos fornecer uma página local de login, e através desta alternativa, consumir o STS requisitando a emissão do token e, consequentemente, reutilizar o token durante toda a sesão do usuário.

Tags: , ,

Security | WCF | WIF

Influencia do maxRequestLength no WCF

by Israel Aece 9. July 2010 10:17

Como eu comentei neste outro artigo, podemos utilizar no WCF um recurso chamado streaming, que é uma alternativa interessante quando precisamos enviar grandes quantidades de informações, que nestes cenários, o mais comum é o envio ou o recebimento de arquivos. Para que tudo isso funcione, precisamos nos atentar a efetuar algumas configurações no WCF com relação as cotas e timeouts, que impõem limites durante o tráfego das informações.

Em princípio, esses são os únicos cuidados que devemos ter. Poderemos começar a ter outros problemas, se esse serviço for hospedado no IIS. Para que fosse possível hospedar um serviço WCF no IIS, a Microsoft utilizou a estrutura do pipeline do ASP.NET para receber e desviar a requisição para serviços WCF para o seu respectivo handler. Antes de qualquer análise das cotas que configuramos para o serviço, há um detalhe importante que devemos nos preocupar. Trata-se da propriedade MaxRequestLength da classe HttpRuntime, que determina a quantidade máxima (em KBytes) de conteúdo que uma requisição do ASP.NET poderá receber, onde o seu valor padrão é 4 MB. Essa propriedade influencia quando queremos efetuar o upload de um arquivo grande através de uma aplicação ASP.NET.

Como a requisição chega para o IIS, que por sua vez delega o processamento para a estrutura do ASP.NET, o mesmo valida o tamanho deste conteúdo, e se for maior do que o valor estipulado nesta propriedade, você receberá uma exceção do tipo HttpException, com a seguinte mensagem: Maximum request length exceeded. É algo até complicado de se diagnosticar, já que essa exceção não é propagada para o cliente que consome o serviço, e o tracing do WCF neste caso, ajudará a desvendar este problema. De qualquer forma, quando for hospedar um serviço WCF no IIS, é importante que você sincronize as cotas com o atributo maxRequestLength exposto pelo elemento httpRuntime, assim como podemos notar no exemplo abaixo, que está configurado para receber um arquivo de até 100 MB:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpRuntime maxRequestLength="102400"/>
  </system.web>
  <system.serviceModel>
    <services>
      <service name="Service">
        <endpoint address=""
                  binding="basicHttpBinding"
                  contract="IService"
                  bindingConfiguration="config" />
      </service>
    </services>
    <bindings>
      <basicHttpBinding>
        <binding name="config"
                 messageEncoding="Mtom"
                 transferMode="Streamed"
                 maxBufferSize="104857600"
                 maxBufferPoolSize="104857600"
                 maxReceivedMessageSize="104857600">
        </binding>
      </basicHttpBinding>
    </bindings>
</configuration>

Tags: , ,

WCF

Expect100Continue em serviços WCF

by Israel Aece 28. March 2010 21:44

Por padrão, todas as requisições HTTP que são relizadas pelas classes que estão contidas no namespace System.Net, adicionam um header chamado "Expect: 100-continue". A finalidade deste header, criado a partir da versão 1.1 do HTTP, quando é implementado corretamente pelos servidores Web, servem para que o servidor, baseando-se apenas nos headers da requisição, determine se a mensagem será ou não aceitada por ele, sem a necessidade de enviar toda a mensagem para depois rejeitá-la.

Quando você tenta consumir um serviço WCF exposto através do protocolo HTTP, eventualmente poderá receber a seguinte mensagem de erro: The remote server returned an unexpected response: (417) Expectation failed. Provavelmente isso pode ser pela má implementação deste recurso do protocolo, que evita conseguirmos consumir o serviço. Para resolver este problema precisamos desativar o envio deste header, que acaba provocando este comportamento. Para isso, podemos recorrer à propriedade estática Expect100Continue da classe ServicePointManager, que recebe um valor boleano (que por padrão é True), indicando se este recurso está ou não habilitado. No nosso caso, devemos definí-la como False antes da chamada da operação do serviço:

System.Net.ServicePointManager.Expect100Continue = false;

Ou, para ficar mais flexível, utilizando o arquivo de configuração:

<system.net>
  <settings>
    <servicePointManager expect100Continue="false"/>
  </settings>
</system.net>

Tags: ,

WCF

Explorando os módulos do WIF para o ASP.NET

by Israel Aece 26. March 2010 15:21

Como vimos no artigo anterior, o WIF fornece vários módulos para acoplarmos ao pipeline do ASP.NET, e que farão tudo o que for necessário para efetuar a comunicação com o respectivo STS. Esses módulos são aqueles tradicionais módulos do ASP.NET, que implementam a interface IHttpModule, e se vinculam à eventos que "cortam" a requisição na vertical.

Esses módulos são: WSFederationAuthenticationModule e SessionAuthenticationModule, e estão debaixo do namespace Microsoft.IdentityModel.Web. O primeiro é responsável pelo processamento do token que é gerado pelo STS, enquanto o segundo persiste essa informação em um cookie, e nas requisições subsequentes, ele irá analisá-lo, e caso não exista ou esteja inválido, redirecionará o usuário novamente para o STS.

Ambas as classes (módulos), fornecem uma série de membros que permite aos desenvolvedores ASP.NET a interceptarem alguns estágios do processamento através de eventos, adicionando um código customizado seja ele para manipulação ou auditoria. Além disso, esses módulos também oferecem alguns métodos e propriedades para permitir a reutilização de algumas funcionalidades/informações. A finalidade deste artigo é explorar os principais membros públicos e o que e como podemos fazer para efetuar tal customização.

Antes de efetivamente falarmos dos membros que são disponibilizados, precisamos saber como temos acesso à cada um deles. Como é responsabilidade do próprio ASP.NET criar os módulos que disponibilizam esses eventos, de algum forma precisamos acessar essas instâncias criadas. Para isso, existe uma classe estática chamada FederatedAuthentication, que também está debaixo do namespace Microsoft.IdentityModel.Web. Essa classe fornece apenas cinco propriedades, também estáticas, a saber:

  • ClaimsAuthorizationModule: Retorna a instância do módulo ClaimsAuthorizationModule. Este módulo é acoplado ao runtime do ASP.NET no evento OnAuthorizeRequest, invocando o método CheckAccess da classe ClaimsAuthorizationManager, que você pode utilizar para customizar a autorização.
  • ClaimsPrincipalHttpModule: Retorna a instância do módulo ClaimsPrincipalHttpModule, que faz uma espécie de tradução, tranformando o principal e identity corrente, em objetos que implementam as interfaces IClaimsIdentity e IClaimsPrincipal.
  • ServiceConfiguration: Essa propriedade retorna uma instância da classe ServiceConfiguration, que representa a seção do arquivo de configuração que corresponde ao WIF.
  • SessionAuthenticationModule: Retorna a instância do módulo SessionAuthenticationModule. Sua funcionalidade já foi discutida acima e neste outro artigo.
  • WSFederationAuthenticationModule: Retorna a instância do módulo WSFederationAuthenticationModule. Sua funcionalidade também já foi discutida acima e neste outro artigo.

A classe FederatedAuthentication ainda fornece um evento ServiceConfigurationCreated, que é disparado depois que a seção do WIF foi totalmente carregada. Agora, depois de conhecermos como podemos chegar até os eventos, podemos recorrer ao modo tradicional do .NET para nos vincularmos à eles, que é através do operador +=. Mas temos também tem uma segunda alternativa, onde podemos definir o tratador do evento utilizando o nome do módulo (classe) e o nome do evento, diretamente dentro do arquivo Global.asax, assim como podemos ver no exemplo abaixo:

<%@ Import Namespace="Microsoft.IdentityModel.Configuration" %>
<%@ Import Namespace="Microsoft.IdentityModel.Web" %>

void WSFederationAuthenticationModule_SignedIn(object sender, EventArgs e)
{
    //...
}

WSFederationAuthenticationModule - Propriedades

Este módulo fornece várias propriedades que ajudam na configuração do redirecionamento para o STS, na geração da mensagem de requisição e no processamento do token emitido pelo STS. Grande parte dessas propriedades acabam sendo configuradas através da seção do WIF (<microsoft.identityModel />) que encontra-se dentro do arquivo Web.config da aplicação que corresponde à relying party.

Entre essas propriedades, temos a PassiveRedirectEnabled. Essa propriedade recebe um valor boleano, que indica ao WIF que ao detectar que o usuário não está autenticado, irá redirecioná-lo automaticamente para o STS configurado na aplicação. É importante dizer que se utilizarmos o controle FederatedPassiveSignIn, essa propriedade deverá estar definida como False, assim como foi comentado neste outro artigo.

Issuer também é uma propriedade importante, que recebe o endereço do STS, para qual o usuário será redirecionado para efetuar a autenticação. A propriedade Reply contém o endereço da relying party, para que o STS possa identificar se ele pode ou não emitir o token para ela.

WSFederationAuthenticationModule - Métodos

Essa classe ainda fornece vários métodos, mas que na maioria das vezes acabam sendo utilizados pelo próprio runtime. O primeiro deles é o método CanReadSignInResponse, que recebe como parâmetro uma requisição (HttpRequest) e retorna um valor boleano indicando se ela se refere à uma mensagem de autenticação, gerada pelo STS. CreateSignInRequest é um método que dado alguns parâmetros, retorna uma instância da classe SignInRequestMessage, que corresponde à mensagem de solicitação de login ao STS.

Ainda temos um método estático, chamado de FederatedSignOut. Esse método tem a finalidade de efetuar a requisição ao STS para efetivar o logout. Já o método SignOut apenas notifica a relying party de que o usuário efetuou o logout. Quando você invoca o primeiro deles, FederatedSignOut, depois de efetuado o logout no STS, o usuário é redirecionado novamente para a relying party, e quando o WIF dectecta que ele fez o logout no STS, invoca o método SignOut, disparando assim os eventos correspondentes para que a relying party seja notificada de que isso realmente aconteceu.

Já o método IsSignInResponse, dado a requisição (HttpRequest), retorna um valor boleano indicando se a requisição trata-se da resposta dada pelo STS. Você pode utilizá-la para burlar ou evitar algum processamento que não deveria acontecer nesta situação. Temos também o método RedirectToIdentityProvider, que como o próprio nome diz, me permite, de forma explícita, redirecionar o usuário para o STS. Isso te dá uma flexibilidade quando você não quer o redirecionamento automático que o WIF faz ao identificar que o usuário não está autenticado.

WSFederationAuthenticationModule - Eventos

O primeiro evento que temos dentro deste principal módulo do WIF é o RedirectingToIdentityProvider. Este evento é disparado antes do usuário ser redirecionado para o STS, dando a oportunidade de customizar o redirecionamento, a mensagem de requisição que é enviada (RST) e a oportunidade de cancelá-la. Tudo isso é fornecido através do argumento RedirectingToIdentityProviderEventArgs, que possui apenas duas propriedades: Cancel e SignInRequestMessage.

Depois deste evento temos os eventos que são disparados durante o recebimento e processamento do token gerado pelo STS, que são: SecurityTokenReceived, SecurityTokenValidated e SessionSecurityTokenCreated. O primeiro é disparado quando a resposta com o token foi enviada pelo STS. Já o segundo, é disparado depois que o token foi validado pelo WIF e o objeto IClaimsPrincipal já foi criado com as claims devolvidas pelo STS, e que podemos ter acesso através do argumento SecurityTokenValidatedEventArgs que é passado para este evento. Finalmente, o evento SessionSecurityTokenCreated, que tem a finalidade de notificar que o token foi recebido, validado e criado, e já está pronto para emitir o cookie para armazená-lo, mas isso depende do valor que é colocado na propriedade WriteSessionCookie, fornecido pelo argumento SessionSecurityTokenCreatedEventArgs. Depois desse estágio, quem faz as manipulações no cookie é o módulo SessionAuthenticationModule, que veremos mais adiante.

Há também eventos que são disparados durante o processo de login, a saber: SignedIn e SignInError. O primeiro evento é disparado depois que o objeto IClaimsPrincipal já foi definido para a thread/requisição corrente, e com isso, seguramente você poderá recorrer as propriedades HttpContext.Current.User e Thread.CurrentPrincipal. O segundo evento, SignInError, é disparado quando algum erro ocorre dentro deste módulo durante a recepção ou validação do token, e você pode utilizar esse evento para verificar a exceção que foi disparada, ter oportunidade de conseguir tratá-la e, eventualmente, catalogar em algum lugar.

Esse módulo ainda fornece mais três eventos que correspondem ao processo de logout: SigningOut, SignedOut e SignOutError. O primeiro é disparado antes do processo de logout iniciar, e segundo ocorre quando o processo de logout foi concluído e, finalmente, o evento SignOutError é disparado se algum problema acontecer durante o logout, e justamente por isso, ele inclui uma propriedade chamada Exception, que retorna a instância da exceção disparada.

E, finalmente, o evento AuthorizationFailed é disparado sempre quando você tenta acessar algum recurso protegido e ainda não está autenticado. Esse evento fornece como argumento um objeto do tipo AuthorizationFailedEventArgs, que por sua vez, disponibiliza uma propriedade chamada RedirectToIdentityProvider, que é do tipo boleana, indicando se o WIF deve ou não redirecionar o usuário para o STS.

SessionAuthenticationModule - Propriedades

A principal propriedade exposta por essa classe é a CookieHandler. Essa propriedade, que expõe um objeto com o mesmo nome, é responsável por armazenar o token gerado pelo STS em um cookie do lado do cliente. Além de gerar o cookie, o módulo SessionAuthenticationModule verifica a existência do mesmo, e caso exista, evita o redirecionamento para o STS em futuras requisições. O cookie handler também é estensível, e veremos mais detalhes sobre isso em um artigo específico.

SessionAuthenticationModule - Métodos

Como essa classe gerencia o cookie que corresponde ao token gerado pelo STS, ela também fornece métodos auxiliares que permitem ter acesso ao cookie em questão. O método ContainsSessionTokenCookie recebe uma coleção de cookies (que na maioria das vezes está dentro do objeto HttpRequest), e retorna um valor boleano indicando se o cookie existe dentro dela. Já o método TryReadSessionTokenFromCookie recebe um parâmetro de saída do tipo SessionSecurityToken, e retorna um valor boleano indicando se foi posssível ler o cookie. Caso retorne True, você pode seguramente acessar o parâmetro de saída, que corresponderá ao token gerado pelo STS.

O método DeleteSessionTokenCookie apenas tem a finalidade de excluir o cookie que corresponde ao token daquele usuário atual. Apesar de público, ele geralmente é invocado durante o processo de logout.

SessionAuthenticationModule - Eventos

Para evitar que o usuário seja redirecionado para o STS a toda requisição que fazemos à alguma página aplicação, esse módulo é adicionado ao runtime do ASP.NET com a finalidade de evitar essa transição a todo momento. Ele verifica se a requisição possui o cookie que corresponde ao token do usuário, que este módulo gerou quando ele (o usuário) se autenticou no STS. Ao detectar a presença deste cookie, ele extrai a informação e permite, através de dois eventos, interceptar o trabalho que ele faz. Esses eventos são: SessionSecurityTokenReceived e SessionSecurityTokenCreated.

Como disse acima, durante a execução este módulo verifica se o cookie está presente, e se estiver, recria o token a partir dele, e após isso, o evento SessionSecurityTokenReceived é disparado. Esse evento fornece como argumento um objeto do tipo SessionSecurityTokenReceivedEventArgs, qual você pode utilizar para re-emitir o cookie, permitindo você a manipular várias características dele, como por exemplo o tempo de expiração do token.

Já o segundo evento, SessionSecurityTokenCreated, é disparado durante a re-emissão do cookie, que muitas vezes acontece quando ele expira. Como argumento, este evento fornece um objeto do tipo SessionSecurityTokenCreatedEventArgs, que fornece duas propriedades: SessionToken e WriteSessionToken. A primeira corresponde ao token do usuário que você pode customizar aqui; já a segunda, recebe um valor boleano indicando se o cookie que irá ser regerado deve ou não ser persistido.

Esse módulo ainda fornece mais três eventos que correspondem ao processo de logout: SigningOut, SignedOut e SignOutError. O primeiro é disparado antes do processo de logout iniciar, o segundo ocorre quando o processo de logout for concluído e, finalmente, o evento SignOutError que é disparado se algum problema acontecer durante o logout, e justamente por isso, ele inclui uma propriedade chamada Exception, que retorna a instância da exceção disparada.

Conclusão: Este artigo mostrou os principais membros dos módulos do WIF, e que você pode fazer uso deles quando o WIF estiver habilitado na aplicação. Como notamos na descrição de cada um dos eventos disponibilizados pelos módulos, eles servem para interceptar vários momentos durante o redirecionamento, validação e persistência do token, e você pode ou não optar por utilizá-los, e isso dependerá da sua necessidade.

Tags: , , , ,

ASP.NET | WIF

Autenticação via Claims no ASP.NET WebForms

by Israel Aece 12. March 2010 23:09

Nos artigos anteriores, falei sobre a proposta da Microsoft para tentar unificar o sistema de autenticação das aplicações. Nos dois primeiros artigos, comentei um pouco sobre os problemas existentes e a teoria que circunda qualquer Identity MetaSystem. Na sequência, introduzi o WIF e alguns tipos que são comuns para qualquer tipo de aplicação que fará uso deste novo framework. A finalidade deste artigo é mostrar como podemos aplicar o WIF em uma aplicação ASP.NET WebForms, analisando tudo o que é necessário para colocar isso em funcionamento, incluindo a criação de um STS para testes.

Quando efetuamos a instalação do SDK do WIF, várias configurações são realizadas dentro da IDE do Visual Studio .NET para suportá-lo. Entre essas configurações, temos a adição de novas templates de projetos, que nos permite criar um projeto com o WIF já pré-configurado, evitando assim lidarmos diretamente com as configurações de baixo nível que ele exige e que veremos no decorrer deste artigo. As quatro templates de projetos disponíveis, estão listadas abaixo com suas respectivas descrições:

  • WCF Security Token Service: Template que traz a configuração padrão para criar um serviço WCF que servirá como um STS no ambiente ativo.
  • ASP.NET Security Token Service Web Site: Template que traz a configuração padrão para criar uma aplicação ASP.NET que servirá como um STS no ambiente passivo.
  • Claims-aware WCF Service: Template que traz um serviço WCF pré-configurado para utilizar algum STS, que será responsável pela autenticação do usuário.
  • Claims-aware ASP.NET Web Site: Template que traz uma aplicação ASP.NET pré-configurada para utilizar algum STS, que será responsável pela autenticação do usuário.

Como disso no parágrafo anterior, o artigo será focado em algumas partes dos bastidores do WIF dentro de uma aplicação ASP.NET (ambiente passivo), e para começar, sabemos que podemos utilizar o atributo mode do elemento authentication no arquivo Web.config, para determinar qual será o modelo de autenticação; esse atributo permite escolhermos um entre as quatro opções disponíveis: None, Forms, Windows e Passport. Abaixo temos um exemplo desta configuração:

<configuration>
  <system.web>
    <authentication mode="Windows" />
  </system.web>
</configuration>

Ao configurar desta forma, automaticamente a aplicação ASP.NET reutilizará a credencial do usuário que está atualmente na máquina, e utilizará essa credencial para refinar o acesso (autorização) e para saber quem o usuário realmente é. Com isso, as propriedades Thread.CurrentPrincipal e HttpContext.User retornam uma instância da classe WindowsPrincipal, que foi criada pelo runtime do ASP.NET, e que corresponde ao usuário atual. Para nos certificarmos disso, podemos recorrer ao seguinte código:

WindowsPrincipal wp = HttpContext.Current.User as WindowsPrincipal;
WindowsIdentity wi = wp.Identity as WindowsIdentity;
Response.Write(wi.Name);

Da mesma forma, se utilizarmos a opção Forms, o runtime do ASP.NET criará uma instância da classe GenericPrincipal e FormsIdentity, que corresponderão ao usuário autenticado através do Forms Authentication, e que na maioria da vezes, esta opção recorre à uma base de dados que serve como repositório para os usuários, onde os serviços de MembershipProvider e de RoleProvider, fornecidos a partir da versão 2.0 do ASP.NET, são uma das várias alternativas disponíveis.

Mas e se queremos preparar nossa aplicação para suportar claims? No artigo Explorando o WIF, eu comentei sobre as interfaces IClaimsIdentity e IClaimsPrincipal, e são elas que devem ser utilizadas pelas aplicações baseadas em claims. Da mesma forma que vimos anteriormente, a criação de classes que implementam essas interfaces ainda continua sendo de responsabilidade do runtime do ASP.NET, que ao receber o token do STS, fará tudo o que for necessário para ler o seu conteúdo e criar os objetos necessários, para que assim determine se o usuário foi devidamente autenticado. Falaremos sobre estes passos mais adiante, ainda neste artigo.

Mesmo que no primeiro momento não queremos envolver um STS para efetuar a validação do usuário, podemos já fazer com que nossa aplicação transforme tudo o que temos atualmente em claims. Por exemplo, se minha aplicação usa autenticação baseada no Windows, então eu menciono WindowsPrincipal na aplicação; já se minha aplicação possui autenticação baseada em Forms, então poderei mencionar GenericPrincipal. Ao invés de lidar diretamente com esses tipos específicos, podemos recorrer as interfaces IClaimsIdentity e IClaimsPrincipal, quais foram desenhadas para atender a essa necessidade, ou seja, independem de tecnologia de autenticação que está sendo utilizada.

Para fazer com que o ASP.NET passe a criar as classes de identity e principal que fazem uso de claims, precisamos acoplar no pipeline do mesmo, um módulo fornecido pelo WIF chamado de ClaimsPrincipalHttpModule, que está debaixo do namespace System.IdentityModel.Web. É importante dizer que os tipos que veremos a partir daqui, estão dentro do assembly Microsoft.IdentityModel.dll, que deverá ser referenciado nas aplicações. Este módulo se vincula ao evento PostAuthenticateRequest (que ocorre assim que o usuário foi identificado), e faz uma espécie de tradução, tranformando o principal e identity corrente, em objetos que implementam as interfaces IClaimsIdentity e IClaimsPrincipal, respectivamente. Durante essa transformação, todos os atributos que eram fornecidos pela tecnologia de autenticação, passam a virar claims, e estão disponívieis para utilização. Para exemplificar o que vimos acima, o primeiro passo é adicionar o módulo no pipeline do ASP.NET, utilizando o elemento httpModules:

<configuration>
  <system.web>
    <authentication mode="Windows" />
    <httpModules>
      <add name="ClaimsPrincipalHttpModule"
           type="Microsoft.IdentityModel.Web.ClaimsPrincipalHttpModule, ..." />
    </httpModules>
  </system.web>
</configuration>

Essa configuração fará com que o ASP.NET faça a transformação do que foi gerado pela tecnologia de autenticação (Windows ou Forms), em tipos que correspondem ao modelo de claims. Sendo assim, o código que utilizamos acima para extrair o nome do usuário a partir da propriedade User da classe HttpContext, pode ser substituído pelo código abaixo. O interessante é que independentemente de que modelo de autenticação estamos utilizando, esse código não irá variar.

IClaimsPrincipal principal = HttpContext.Current.user as IClaimsPrincipal;
IClaimsIdentity identity = principal.Identity as IClaimsIdentity;

foreach (var item in identity.Claims)
    Response.Write(string.format("{0}: {1}<br />", item.ClaimType, item.Value));

Envolvendo um STS

No exemplo que vimos acima, ele somente se preocupa em fazer com que todos os modelos de autenticação suportados pelo ASP.NET, sejam automaticamente traduzidos para claims, nada mais. Até então, a autenticação continua sob responsabilidade desta mesma aplicação. Como podemos proceder para "externalizar" a autenticação? Será justamente este assunto que está seção irá abordar, entendendo como configurar a aplicação para conseguir receber os tokens que serão gerados por um terceiro, o STS.

Em uma aplicação ASP.NET tradicional, e que faz uso do modelo Forms para autenticação, quando tentamos acessar uma página protegida e não estamos autenticado, o ASP.NET automaticamente irá nos redirecionar para uma página de login que é definida no arquivo Web.config, colocando na Url uma query string chamada ReturnUrl, que contém a página que tentamos acessar. Esse parâmetro é interessante, pois se formos devidamente autenticado, seremos redirecionado para ela.

Quando envolvemos um STS, o processo é bem semelhante, mas ao invés dele redirecionar para uma página local que efetuará o login, devemos ser redirecionados para um outro site, que por sua vez, fará a validação e também a emissão do token, redirecionando o usuário para aplicação - protegida - que ele tentou acessar. Como vimos nos artigos anteriores, esse é o ambiente passivo, onde o navegador irá atuar apenar para coordenar os redirecionamentos entre as aplicações (STS e relying party). A imagem abaixo ilustra como o processo deverá acontecer:

Analisando a imagem acima, repare que o usuário solicita uma aplicação ASP.NET que está protegida (1). Essa aplicação verifica que o usuário não está autenticado. Como ela não é mais responsável por autenticá-lo, ela redireciona o usuário para o STS em que ela confia (2). O usuário se autentica no STS utilizando a tecnologia que foi definida lá, que pode ser Kerberos (Windows), UserName (Forms Authentication), certificados, etc. (3). Se o usuário for válido, um token é emitido para aquele usuário (4). Finalmente, o navegador redireciona o usuário para a página solicitada, mas agora incluindo o token (5).

Toda a mágica que acontece neste processo, acaba sendo realizada através de funcionalidades que o próprio protocolo HTTP fornece, ou seja, faz uso de requisições GET e POST, anexando na URL alguns parâmetros que são utilizados para garantir que tudo funcione conforme o esperado. Antes de analisarmos o conteúdo que é trafegado entre as partes, vamos nos concentrar em como devemos configurá-las para que essa conversação seja possível.

Antes de criar a aplicação que servirá como relying party, vamos optar por iniciar pelo STS. No início do artigo, vimos as templates de projeto que são criadas na instalação do SDK do WIF, e uma delas é justamente uma aplicação ASP.NET que serve como um STS (ASP.NET Security Token Service Web Site). A ideia desta template, é permitir você criar uma aplicação que valida usuários e emita tokens para ele. Um cenário em que ele poderia ser utilizado, é se você tiver uma estrutura de Membership, Roles e Profile, e quer que este STS valide os usuários e faça a emissão de tokens baseando-se nessa estrutura. Como sabemos, a vantagem disso é que todas as aplicações podem confiar neste STS, centralizando assim toda a lógica necessária para isso.

A aplicação STS gerada pela template, possui três arquivos na raiz do projeto: Default.aspx, Login.aspx e Web.config. No arquivo Web.config, não há nada especial, nada que não conhecemos. A configuração inicial faz com que o atributo mode do elemento authentication seja definido como Forms, pois será esse o modo de autenticação que os usuários usarão lá. Neste mesmo arquivo de configuração, a página Login.aspx é definida como a página onde os usuários deverão informar suas credenciais, e a página Default.aspx é proibida de ser acessada por usuários anônimos.

<configuration>
  <system.web>
    <authentication mode="Forms">
      <forms loginUrl="~/Login.aspx" />
    </authentication>
    <authorization>
      <deny users="?" />
    </authorization>
  </system.web>
</configuration>

Além do que vimos acima, dentro do projeto criado para servir como STS, ele ainda fornece o diretório App_Code, com algumas classes dentro. As classes que constam ali, principalmente a CustomSecurityTokenService, é o nosso STS em si. Repare que ele herda diretamente da classe SecurityTokenService, que fornece toda a estrutura necessária para a criação de um STS customizado. Ele fornece dois métodos chaves que devem ser sobrescritos nas classes derivadas, a saber: GetScope e GetOutputClaimsIdentity.

O primeiro método, GetScope, nos dá a possibilidade de verificar quais são as chaves de criptografia que será utilizada, baseando-se no endereço da relying party. Esse método retorna uma instância da classe Scope, que contém toda a configuração necessária para gerar o retorno para a respectiva aplicação (relying party). Já o segundo método, GetOutputClaimsIdentity, é o local onde efetivamente definimos quais claims serão criadas para aquele usuário recém autenticado, e que na maioria das vezes, iremos recorrer a uma busca no banco de dados para extrair esses dados. Esse método retorna a instância de uma classe ClaimsIdentity, que implementa a interface IClaimsIdentity, e como sabemos, fornece uma propriedade chamada Claims, que nada mais é que uma coleção, onde podemos adicionar quantas claims desejarmos.

Ambos métodos recebem como parâmetro um principal, do tipo IClaimsPrincipal, que corresponde ao principal criado para o chamador do STS, e além dele, um outro parâmetro chamado request, do tipo RequestSecurityToken, que representa a requisição enviada ao STS (RST), e que pode ser utilizada para extrair informações adicionais.

Note que a validação do usuário acaba sendo realizada através da tecnologia que utilizamos para isso, que neste caso, é a Forms Authentication. No code-behind do arquivo Login.aspx, temos a validação acontecendo, que deve recorrer à alguma base de dados, como o MembershipProvider, para validar o usuário. Somente a partir daí é que a classe do STS que vimos acima entrará em cena.

Criação da Relying Party

Na seção anterior, expliquei como estruturar a aplicação que servirá como STS. Uma vez que ela está criada, precisamos agora desenvolver as aplicações que servirão como relying party, ou melhor, aplicações que utilizarão aquele STS para autenticar o usuário. Da mesma forma que fizemos para criar um STS, vamos recorrer a a template de projeto chamada Claims-aware ASP.NET Web Site para criar a relying party. Este projeto já traz a estrutura necessária para que a aplicação já consuma um STS, mas como fizemos acima, vamos entender a mágica que está por trás disso tudo.

Do lado da aplicação ASP.NET que será uma relying party, pequenas mudanças são necessárias no arquivo de configuração (Web.config), mas que impactam diretamente em como o runtime do ASP.NET irá tratar os redirecionamentos necessários. Da mesma forma que já fazemos no ASP.NET tradicional, precisamos negar o acesso aos usuários anônimos, aquelas páginas que necessitam que eles estejam devidamente autenticados. Para isso, utilizamos o elemento authorization, assim como é mostrado abaixo:

<configuration>
  <system.web>
    <authentication mode="None" />
    <authorization>
      <deny users="?" />
    </authorization>
  </system.web>
</configuration>

Note também que a autenticação é desligada nesta aplicação, pois não compente mais a ela validar o usuário. Mas isso por si só não é suficiente para que esta aplicação contate o STS. Ainda é necessário especificar algumas outras informações que conduzirá a relying party à como efetuar o redirecionamento para o STS, e para isso, há uma seção exclusiva do WIF, que nos permite configuar cada um dos parâmetros exigidos. O código abaixo ilustra a configuração de uma relying party que confia no STS que criamos acima:

<microsoft.identityModel>
  <service>
    <audienceUris>
       <add value="https://aplicacoes.minhaempresa.com.br/App01/" />
    </audienceUris>
    <applicationService>
       <claimTypeRequired>
          <claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
                     optional="true" />
          <claimType type="http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
                     optional="true" />
       </claimTypeRequired>
    </applicationService>
    <federatedAuthentication>
      <wsFederation passiveRedirectEnabled="true"
                    issuer="https://administrativo.minhaempresa.com.br:444/STS01/"
                    realm="https://aplicacoes.minhaempresa.com.br/App01/"
                    requireHttps="true" />
      <cookieHandler requireSsl="true" />
    </federatedAuthentication>
    <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, ...">
      <trustedIssuers>
        <add thumbprint="0844D588E25237D81AC61BF7C38EDAB7075BD024"
             name="https://administrativo.minhaempresa.com.br:444/STS01/" />
      </trustedIssuers>
    </issuerNameRegistry>
  </service>
</microsoft.identityModel>

Primeiramente precisamos registrar a seção microsoft.identityModel no arquivo de configuração, através do elemento configSections, pois ela não consta no schema de configuração do .NET Framework. Uma vez registrada, podemos fazer uso dela na mesma aplicação. Imediatamente dentro desta seção, temos uma sub-seção chamada de service que envolverá todas as configurações de autenticação daquela aplicação.

A partir de agora, temos outras sub-seções que efetivamente configuram o comportamento do WIF, e como podemos ver, temos uma seção chamada audienceUris. Dentro desta seção, podemos elencar uma coleção de URIs, que definem os possíveis endereços em que o token poderá ser entregue, evitando que clientes maliciosos façam uso do mesmo. Já o próximo elemento, applicationService/claimTypeRequired, tem um papel importante, que diz ao STS quais claims a aplicação necessita, podendo você determinar, através do atributo optional, se ela é ou não opcional.

Em seguida, vemos um elemento chamado federationAuthentication. Este elemento nos permite configurar as informações referente ao STS em que a aplicação confia. O sub-elemento wsFederation fornece alguns atributos que nos auxiliam nisso. O primeiro deles é o issuer, onde devemos especificar o endereço do STS; já no atributo realm informamos o endereço da aplicação solicitante, e que coincide com o endereço especificado no elemento audienceUris. E ainda, temos o atributo passiveRedirectEnabled, que recebe um valor boleano indicando se o WIF deve efetuar o redirecionamento automático quando detectar que o usuário não está autenticado. Veremos um cenário mais claro para este atributo quando falarmos dos controles fornecidos pelo WIF. Ainda debaixo do elemento federationAuthentication, temos o sub-elemento chamado de cookieHandler, que fornece alguns atributos para configurar o cookie que corresponderá a sessão de segurança de um determinado usuário.

Para finalizar este grande bloco de configuração, temos ainda uma última seção, chamada de issuerNameRegistry. Como o próprio nome diz, permite especificarmos ali os STSs em que a aplicação confia. É importante dizer que cada STS (issuer) é conhecido através do thumbprint de um certificado X.509. Note também que esta seção especifica o tipo da classe que é responsável por efetuar a validação do STS. A classe é definida através do atributo type, e no exemplo acima, a classe que utilizamos já está embutida no WIF, e é chamada de ConfigurationBasedIssuerNameRegistry, que tem a finalidade de extrair do arquivo de configuração as informações necessárias para efetuar tal validação. Caso queira customizar isso, basta você herdar da classe abstrata IssuerNameRegistry, sobrescrevendo o método GetIssuerName.

Depois de todas as configurações que vimos acima, isso ainda não funciona sozinho, pois ainda não há nada que mude a execução padrão do ASP.NET. É justamente aqui que introduzimos duas novas classes, chamadas de WSFederationAuthenticationModule e SessionAuthenticationModule (namespace Microsoft.IdentityModel.Web). Ambas classes são módulos (implementam a interface IHttpModule), e devem ser acoplados na execução do ASP.NET, para que o WIF entre em ação. Esses módulos irão consumir grande parte das configurações que vimos acima, para que consiga efetuar todas as tarefas, tais como: redirecionamento, criptografia e deserialização do token gerado pelo STS.

Uma vez que o módulo WSFederationAuthenticationModule é adicionado ao pipeline do ASP.NET, ele se vincula ao evento AuthenticateRequest. Ao tentar acessar uma página protegida e não estiver autenticado, esse módulo irá detectar que o acesso foi negado (status 401 do HTTP), e redirecionará o usuário para o respectivo STS. Depois que o usuário for validado e o STS emitir o token para ele, novamente ele será redirecionado novamente para a aplicação (RP), onde este mesmo módulo interceptará e irá utilizar este token para criar a instância da classe IClaimsPrincipal, atribuindo-a a propriedade User da classe HttpContext. Esse módulo encapsula todas as complexidades dos padrões WS-* que são utilizados para efetuar a comunicação entre a aplicação e o autenticador.

Se a cada página que tentássemos acessar fossemos redirecionado para o STS, isso seria algo totalmente inviável. Sendo assim, é necessário que uma vez autenticado, essa sessão seja mantida durante as requisições que fazemos para esta mesma aplicação. É justamente neste momento que entra em cena o módulo SessionAuthenticationModule. A finalidade dele é monitorar o cookie que é criado e representa o usuário que foi autenticado pelo STS. Caso esse cookie esteja presente e seja válido, esse módulo faz o trabalho necessário para manter a propriedade User da classe HttpContext configurada, sem precisar efetuar - novamente - a visita ao STS. Abaixo temos os dois módulos adicionados (elemento httpModules) ao runtime do ASP.NET na relying party:

<httpModules>
  <add name="WSFederationAuthenticationModule"
       type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, ..." />
  <add name="SessionAuthenticationModule"
       type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, ..." />
</httpModules>

É importante dizer que além destes módulos modificarem a execução de uma aplicação ASP.NET, eles fornecem uma série de membros (métodos, eventos e propriedades) que podem ser utilizados pela aplicação, mas esses membros serão abordados em um futuro artigo.

Mas estes dois módulos não são os únicos elementos que são executados durante o processo de autenticação do usuário. Internamente eles fazem uso de diversas outras classes que possuem um papel importante ou que servem apenas como um ponto de estensibilidade. Para ter uma visão mais clara disso, analise a imagem abaixo, que foi baseada em uma apresentação do Vittorio Bertocci durante o PDC 2009:

A classe SecurityTokenHandler (namespace Microsoft.IdentityModel.Tokens) que vemos depois do módulo WSFederationAuthenticationModule, é uma classe abstrata e serve como base para outros token handlers. Token handler, como o próprio nome diz, é o responsável por validar o token gerado pelo STS, que dependendo do tipo de autenticação utilizado, o token pode estar formatado de uma maneira/versão específica. Como essa classe é abstrata, há várias implementações dela debaixo deste mesmo namespace, onde cada implementação visa um formato diferente (Saml, UserName, etc.), e o qual será autenticado depende da requisição.

Na sequência visualizamos a classe ClaimsAuthenticationManager. Por padrão, essa classe não faz absolutamente nada, mas dá a chance de customizá-la através do método virtual Authenticate, permitindo interceptar a primeira requisição, onde o token gerado pelo STS é enviado para a aplicação. Com ela, podemos customizar e validar as claims que foram retornadas pelo STS, fazendo alguma validação, adicionando novas claims ou até mesmo fazendo transformações/traduções.

Finalmente, temos a classe ClaimsAuthorizationManager, que permite definir um lugar centralizado para definir as regras de autorização. Essa classe também fornece um método chamado CheckAccess, que retorna uma valor boleano indicando se o acesso será ou não concedido. Para poder tomar a decisão para conceder ou negar o acesso, este método recebe como parâmetro uma instância da classe AuthorizationContext, que contém os seguintes membros: Action, Principal e Resource. Estensibilidade é um assunto longo e complexo, e que não será abordado neste artigo, pois exige um artigo exclusivo para isso.

Assistente para Configuração

A ideia de toda a explicação acima foi tentar mostrar os passos necessários para configurar, manualmente, tudo o que é necessário para acoplar o WIF no ASP.NET, abordando os principais elementos que devem estar presentes nos arquivos de configuração da relying party e do STS. Só que configurar isso manualmente pode ser complexo e propício a erros, pois devido a quantidade de informações que devemos definir, eventualmente podemos esquecer de algo importante, que evitará do WIF funcionar adequadamente.

Para tornar a configuração de ambas as partes simples e intuitiva, a Microsoft disponibiliza um utilitário chamado Federation Utility (FedUtil.exe), que é instalado juntamente com o SDK do WIF. Esse assistente tem a finalidade de nos guiar durante a configuração de uma relying party. Quando instalamos o SDK, ao clicar com o botão direito do mouse em cima de um projeto ASP.NET, uma nova opção está disponível, chamada Add STS Reference. O utilitário é inicializado a partir desta aplicação, e nos ajudará a escolher/criar um STS. As imagens abaixo ilustram os dois primeiros passos disponíveis:

Na primeira imagem definimos o caminho físico até o arquivo Web.config, e logo abaixo temos a URI desta mesma aplicação (relying party). O passo a seguir, que está ilustrado na imagem subsequente, oferece três opções para a configuração do STS, e dependendo da opção escolhida, refletirá em configurações específicas no arquivo Web.config mencionado no primeiro passo. Abaixo temos a lista com as três opções e suas respectivas descrições:

  • No STS: Ao selecionar esta opção, tudo o que será feito na aplicação é a inserção do módulo ClaimsPrincipalHttpModule, conforme discutimos no início do artigo. Como sabemos, isso habilitará que qualquer tipo de autenticação que seja utilizado no projeto, seja transformado em claims.
  • Create a new STS project in the current solution: Esta opção cria na mesma solução um projeto ASP.NET que servirá como um STS, e depois disso, já irá alterar o arquivo Web.config da relying party, fazendo com que ela confie neste STS recém criado.
  • Use an existing STS: Como podemos notar, essa opção permite especificarmos um endereço de um STS existente, em que a aplicação corrente irá confiar.

Avançado alguns passos deste assistente, há uma tela em que são listadas as claims oferecidas por aquele STS, tela qual você pode avaliar se aquele STS fornece as claims necessárias para a sua aplicação, permitindo você evitar a referência à este STS caso ele não atenda. A imagem abaixo exibe esta tela informativa:

Controles

Ao referenciar o assembly que corresponde ao WIF em uma aplicação ASP.NET, automaticamente dois controles são adicionados à toolbox do Visual Studio: FederatedPassiveSignInStatus e FederatedPassiveSignIn, onde ambos estão debaixo do namespace Microsoft.IdentityModel.Web.Controls. Assim como já acontece nos controles de autenticação do ASP.NET, o controle FederatedPassiveSignInStatus alterna a exibição de um HyperLink contendo o endereço para efetuar o login ou o logout, dependendo do status atual do usuário.

Recapitulando o que foi falado acima, quando o WIF detecta que o usuário não está autenticado, automaticamente irá redirecioná-lo para o STS, e tudo isso graças ao atributo passiveRedirectEnabled do elemento wsFederation, que quando estiver definido como True, terá esse comportamento. Mas imagine que você, por algum motivo, confie em mais do que um STS, ou ainda, quer dar a opção ao usuário dele se autenticar usando o STS ou um outro formato mais tradicional, como o próprio Membership. Neste caso, o redirecionamento não poderá acontecer automaticamente, e é neste momento que entra em cena o controle FederatedPassiveSignIn.

Neste caso, a relying party terá uma página que permitirá ao usuário escolher o modo de autenticação desejado, e o controle em questão, permitirá configurarmos todas as informações pertinentes ao STS que ele referencia. As suas principais propriedades são: Issuer e Realm, onde você deve definir o endereço do autenticar e da aplicação que o utiliza, respectivamente; ainda temos uma outra propriedade chamada SignInMode, que aceita duas opções expostas pelo enumerador SignInMode: Session e Single. A primeira opção (padrão) fará com que um cookie seja criado para persistir a sessão do usuário, enquanto a opção Single é utilizada apenas para uma única requisição. Abaixo temos o código ASPX correspondente ao controle FederatedPassiveSignIn, com as propriedades definidas, mas para que ele funcione de forma correta, é necessário definir o atributo passiveRedirectEnabled do elemento wsFederation como False.

<%@ Register
    Assembly="Microsoft.IdentityModel, ...."
    Namespace="Microsoft.IdentityModel.Web.Controls"
    TagPrefix="wif" %>

<wif:FederatedPassiveSignIn
    ID="FederatedPassiveSignIn1"
    runat="server"
    Issuer="https://administrativo.minhaempresa.com.br:444/STS01/"
    Realm="https://aplicacoes.minhaempresa.com.br/App01/"
    SignInMode="Session" />

Metadados

Para referenciar um serviço em uma aplicação, geralmente recorremos ao documento WSDL que ele fornece. Dentro deste documento há uma série de informações, que descrevem grande parte das capacidades do serviço, tais como: as operações fornecidas, o modelo de segurança, entre outras coisas. De forma análoga, o STS precisa informar para aquelas aplicações que desejam utilizá-lo, o que ele fornece, como por exemplo: endereço do mesmo para a validação do usuário, quais claims estão disponíveis, certificado usado para a proteção das mensagens que são trocadas, etc.

Todas essas informações são fornecidas através de um documento XML, localizado fisicamente no STS, e que todas as aplicações que desejam consumí-lo, podem e devem consultar esse arquivo para conhecer as informações necessárias para efetuar a configuração local (aquelaque encontra-se na seção microsoft.IdentityModel no arquivo Web.config). Ao rodar o utilitário FedUtil.exe, ele consulta esse documento para extrair as informações e efetuar a configuração necessária no arquivo Web.Config informado no primeiro passo do assistente.

Esse arquivo tem o nome de FederationMetadata.xml, e fica contido em uma pasta chamada FederationMetadata/2007-06, mas também é criado na aplicação que referencia o STS, com uma estrutura um pouco diferente. A ideia deste arquivo de metadado do lado do cliente, é fornecer ao STS o endereço da aplicação, podendo assim, o STS configurar as aplicações para quais ele poderá emitir tokens.

Estrutura da Requisição e Resposta

Quando utilizamos FormsAuthentication em um projeto ASP.NET, ao tentar acessar uma página protegida quando não estamos autenticados, somos redirecionados para a página de login especificada no Web.config, e um item chamado ReturnUrl é adicionado como query string, identificando a página que foi solicitada, para que se o usuário for autenticado, automaticamente será redirecionado para a mesma.

Ao utilizar o WIF, teremos algo semelhante, mas ao ser redirecionado para o STS que validará o usuário, vários parâmetros serão adicionados como query string, para que o STS possa utilizá-los durante o processo de validação, geração de tokens e, finalmente, o redirecionamento de volta para a aplicação. Esses parâmetros são definidos através da especificação do WS-Federation, que rege a estrutura das mensagens. Abaixo temos a lista dos possíveis parâmetros utilizados para efetuar a requisição (RST), com suas respectivas descrições:

  • wa: Este parâmetro identifica a action a ser executada pelo STS. Esse parâmetro pode receber dois valores, sendo o primeiro o wssignin1.0 para instruir o STS a a validar e gerar o token para o usuário. Já o segundo, wssignout1.0, diz ao STS para proceder o logout do usuário.
  • wtrealm: Representa a URI da aplicação que está solicitando a autenticação do usuário (relying party). O STS usará essa URI para identificar a aplicação solicitante e, eventualmente, efetuar alguma verificação interna, customizando as claims geradas para cada uma delas. Além disso, esse parâmetro serve também para que o STS saiba para onde deverá responder.
  • wctx: Este parâmetro define a URL original que o usuário tentou acessar, e assim como a ReturnUrl do ASP.NET, esse parâmetro serve para redirecionar o usuário para o local exato dentro da aplicação (relying party), depois da autenticação realizada.
  • wct: Este parâmetro indica o horário atual na aplicação solicitante, garantindo assim que o cálculo das eventuais expirações sejam feitas de forma coerente, já que o STS pode estar em um fuso, enquanto a relying party pode estar em outro.

Uma vez redirecionado, o STS fará o que for necessário para validar o usuário, e depois que isso for realizado, novamente o usuário será redirecionado devolta para a aplicação que ele tentou acessar. Neste momento, ao invés de ser uma requisição normal, através do método GET, um POST será efetuado. Isso se deve pelo fato de que o token gerado pode ser maior que 4KB, e isso é uma limitação imposta pelos navegadores na URL, impedindo que o método GET seja utilizado. Quando o POST é feito para a aplicação (relying party), o módulo WSFederationAuthenticationModule entra em cena, processando a resposta gerada pelo STS, como já vimos acima.

Para exemplificar, abaixo temos o endereço da requisição que foi montado pelo WIF, redirecionando o usuário para o STS referenciado com os parâmetros que identificam a relying party.

https://administrativo.minhaempresa.com.br:444/STS01/?
    wa=wsignin1.0&
    wtrealm=https%3a%2f%2faplicacoes.minhaempresa.com.br%2fApp01%2f&
    wctx=rm%3d0%26id%3dpassive%26ru%3d%252fApp01%252fDefault.aspx&
    wct=2010-03-12T00%3a55%3a37Z

Como vimos acima, uma vez que o usuário é autenticado e o token gerado, o STS efetua um POST para a aplicação solicitante. O conteúdo desta resposta é um HTML com um formulário (tag <form />), e dentro dele há vários campos ocultos, que representam os parâmetros do WS-Federation para a resposta (RSTR), e inclui também um pequeno código JavaScript para efetuar uma espécie de um auto-submit o formulário. Os parâmetros do WS-Federation utilizados aqui são:

  • wa: Contém a ação executada pelo STS.
  • wctx: Contém a URL original que o usuário tentou acessar na aplicação solicitante.
  • wresult: O resultado em si, que contém o token emitido pelo STS.

SSO - Single Sign-On

Como comentado acima, o WIF utiliza cookies no ambiente passivo para evitar que o usuário seja redirecionado a todo momento para o STS. Como sabemos, há uma regra em que uma aplicação (site) somente pode acessar os cookies que foram emitidos por ela, em nome daquele domínio em que ela se encontra. Mas dessa forma, como podemos garantir o Single Sign-On (SSO)? (SSO é a possibilidade de se autenticar uma única vez, em um local único, e assim poder acessar todas as aplicações que confiam neste autenticador, sem a necessidade de efetuar novamente o login)

Para que isso seja possível, o cookie pode ser gerado para o domínio da relying party ou para o domínio do STS. A segunda opção é a utilizada para garantir o SSO, já que o cookie é gerado em nível de STS e somente a partir daquele domínio é que poderemos acessá-lo. Uma vez que você possui várias aplicações confiando neste STS, os redirecionamentos são realizados até ele, mas ao detectar a presença de um cookie válido para aquele token, o formulário de login não será apresentando, redirecionando o usuário devolta para a aplicação solicitante, garantindo assim um ambiente confiável.

Conclusão: Neste extenso artigo, vimos como funciona os bastidores e como configuar o WIF em uma aplicação ASP.NET Web Forms. Como a Microsoft se preocupou em manter o mesmo modelo de programação quando falamos de autenticação e autorização, o simples fato de acoplarmos um dos módulos que vimos acima no pipeline do ASP.NET, já faz com que magicamente a identidade do usuário seja transformada em claims, e toda a complexidade de redirecionamentos entre as aplicações (relying parties e STSs) também já está embuitida nestes módulos. E como se não bastasse, ainda há o utilitário FedUtil.exe que nos auxilia na configuração, evitando conhecer o schema do WIF. Além disso, o WIF com ASP.NET facilita a implementação de SSO, tarefa que é extremamente árdua sem o uso dele.

Tags: , , ,

ASP.NET | Security | WIF

Gerenciamento de Channels

by Israel Aece 1. February 2010 12:38

Algum tempo atrás eu falei sobre os internals de um proxy WCF. Como eu havia dito, existe um grande overhead quando criamos um novo proxy, que está condicionado as complexidades que podem ou não estarem habilitadas (segurança, transações, mensagens confiáveis, etc.).

Como disse naquele mesmo post, o ponto mais custoso da criação, que é a factory (ChannelFactory<TChannel>), está sendo reutilizada, em nível de AppDomain, ou seja, mesmo que você não mantenha a instância do proxy gerado (ClientBase<TChannel>) pela IDE do Visual Studio ou pelo utilitário svcutil.exe, o runtime do WCF irá reciclar a factory, e em seguida, a colocará em um cache.

Quando estabelecemos a comunicação entre o cliente e o serviço, utilizamos um canal de comunicação, referido também como channel. Estes são utilizados por nós, na maioria das vezes implicitamente, para efetuar as requisições para o respectivo serviço. Na verdade, é a factory que fornece um método chamado CreateChannel, que retorna a instância de um TransparentProxy. Este tipo nos permite efetuar uma conversão para o contrato exposto pelo documento WSDL, e assim invocar as operações como se elas fossem simples métodos locais. Se você abrir a classe que é gerada quando você faz a referência à um serviço, verá que dentro das operações sempre há uma chamada para a propriedade Channel, que por sua vez, irá até o método CreateChannel.

Como a parte custosa já está sendo reutilizada de forma performática pelo WCF, as vezes surge a dúvida se devemos ou não manter um outro cache, mas este para armazenar as instâncias dos channels que são retornados pelo método CreateChannel, mas que a classe ClientBase<TChannel> não se preocupa com isso. O objeto retornado por esse método implementa a interface ICommunicationObject, que fornece entre vários membros, um método autoexplicativo chamado Close, que é responsável por encerrar mover o estado do proxy corrente para Closed e também descartar os recursos que ele utiliza.

Assim como todo recurso caro, é importante você sempre descartá-lo quando não precisar mais dele. Isso também é o caso dos channels. Mas é importante você analisar cuidadosamente o cenário. Se você faz chamadas subsequentes, não convém a todo momento invocar o método CreateChannel para criar um novo canal; reutilize-o durante essas chamadas, e somente feche-o quando realmente não precisar mais dele, dentro daquele escopo.

Da mesma forma, antes de descartá-lo completamente, analise se você não pode manter um cache para estes channels. Fazer pooling de channels é uma boa alternativa para diminuir ainda mais os custos de criação deles. Você pode manter os channels desde que:

  • Não há um contexto de segurança exclusivo para cada usuário (SCT).
  • O mesmo channel não é utilizado por múltiplas threads ao mesmo tempo.
  • Não há manutenção de estado.

Na primeira situação o problema acontece porque as credenciais são definidas durante a criação da factory, e não podem mais serem alteradas. Já o segundo cenário, acontece porque os channels são thread-safe, ou seja, eles não dão suporte ao envio de mensagem de forma concorrente. Se em uma thread A você está utilizando o channel C1 para mandar uma mensagem grande, a thread B que utilizará o mesmo channel C1, somente conseguirá enviar a mensagem depois que a thread A finalizar. A última das situações implica quando você mantém uma sessão entre o cliente e o serviço, pois ela está fortemente ligada ao channel correspondente.

Tags: ,

WCF

Compartilhamento de Bindings

by Israel Aece 30. November 2009 08:25

Quando você possui múltiplos endpoints para expor um serviço, na maioria das vezes, esses endpoints possuem características diferentes, pois você poderá publicá-lo através de HTTP para consumo externo, enquanto as aplicações locais, consomem este mesmo serviço através de TCP, para uma melhor performance.

A configuração abaixo ilustra a exposição de um mesmo serviço e de um mesmo contrato através dos protocolos TCP e HTTP. Cada um dos protocolos possuem características diferentes de comunicação, e justamente por isso, exigem bindings diferentes.

<system.serviceModel>
  <services>
    <service name="App.Servico">
      <host>
        <baseAddresses>
          <add baseAddress="net.tcp://localhost:9838"/>
          <add baseAddress="http://localhost:8478"/>
        </baseAddresses>
      </host>
      <endpoint address="srv"
                contract="App.IContrato1"
                binding="netTcpBinding"/>
      <endpoint address="srv"
                contract="App.IContrato1"
                binding="basicHttpBinding"/>
    </service>
  </services>
</system.serviceModel>

Cada endpoint é composto por um endereço, binding e contrato, e cada endpoint possui seu próprio listener e channel stack, que varia de acordo com o binding escolhido. Em algumas situações, é comum ter mais do que um contrato para o mesmo serviço, onde queremos compartilhar o mesmo endereço para ambos contratos. Abaixo podemos ver um serviço que tem esse comportamento:

<system.serviceModel>
  <services>
    <service name="App.Servico">
      <host>
        <baseAddresses>
          <add baseAddress="net.tcp://localhost:9838"/>
        </baseAddresses>
      </host>
      <endpoint address="srv"
                contract="App.IContrato1"
                binding="netTcpBinding"/>
      <endpoint address="srv"
                contract="App.IContrato2"
                binding="netTcpBinding"/>
    </service>
  </services>
</system.serviceModel>

Repare que temos dois contratos (IContrato1 e IContrato2) expostos através de TCP e usando o mesmo endereço. Isso somente é possível porque o WCF reutiliza a mesma instância do binding para efetuar a comunicação, e difere a execução baseando-se nas Soap Actions. Se cada binding tiver sua configuração específica (definido através do atributo bindingConfiguration), então isso fará com que o WCF crie instâncias diferentes do mesmo binding, resultando em uma exceção tipo do InvalidOperationException sendo disparada, informando que não é permitido associar uma mesma instância binding à endereços diferentes.

Se você tiver endereços distintos, então duas instâncias distintas do binding correspondente serão criadas para atender cada um deles. Esse mesmo cuidado você precisa ter ao configurar o serviço de forma imperativa. Ao chamar o método AddServiceEndpoint da classe ServiceHost, você deverá se preocupar em passar a mesma instância do binding, como por exemplo:

using (ServiceHost host = 
       new ServiceHost(typeof(Servico), new Uri[] { new Uri("net.tcp://localhost:9838") }))
{
    NetTcpBinding b = new NetTcpBinding();

    host.AddServiceEndpoint(typeof(IContrato1), b, "srv");
    host.AddServiceEndpoint(typeof(IContrato2), b, "srv");

    host.Open();
    Console.ReadLine();
}

Tags: , , , ,

WCF

Powered by BlogEngine.NET 1.5.0.0
Theme by Mads Kristensen

Sobre

Meu nome é Israel Aece e sou especialista em tecnologias de desenvolvimento Microsoft, atuando como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. [ Mais ]

Twitter

Host