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

Autenticação via Claims no WCF

by Israel Aece 13. July 2010 22:33

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 serviços WCF, analisando tudo o que é necessário para colocar isso em funcionamento, incluindo a criação de um STS customizado para testes.

Em um artigo anterior, vimos como podemos implementar o WIF à aplicações ASP.NET, que recorrem ao navegador para serem apresentadas. Com isso, o WIF ou qualquer outro framework de autenticação, faz uso das funcionalidades que o navegador disponibiliza, para assim, coordenar de forma passiva, tudo o que é necessário para efetuar a autenticação em uma segunda aplicação.

Quando falamos em autenticação de serviços, o processo muda um pouco, pois geralmente eles são acessados através de uma aplicação cliente, que muitas vezes deve se identificar antes de consumir as funcionalidades que ele expõe. Neste cenário podemos perceber que não há o navegador, e sim uma aplicação (na maioria das vezes, desktop (Windows Forms ou WPF)) que consomem o serviço.

No artigo anterior discutimos o ambiente passivo, e aqui entra o ambiente ativo. O nome se deve ao fato de que a aplicação cliente coordena ativamente o processo de autenticação do usuário quando ele, por sua vez, precisar acessar um serviço que exija que o usuário esteja devidamente autenticado para poder acessá-lo. Como já acontecia anteriomente, os serviços que compõem a sua estrutura, pode seguramente confiar em um orgão que efetua a autenticação, evitando assim configurarmos serviço por serviço todo o processo necessário para a autenticação do usuário.

Como o WCF já traz vários pontos de estensibilidade, a Microsoft apenas introduziu nesses locais recursos para interceptar o processo de autenticação, e com isso, acoplando o WIF para efetuar todo o trabalho. A ideia aqui é ao configurar um serviço WCF, dizer que ele somente permitirá usuários autenticados por uma outra aplicação, que também será um serviço, mas construído com os recursos fornecidos pelo WIF.

Ao expor um serviço WCF neste caso, a configuração do mesmo deverá conter entre vários itens, o autenticador. Com isso, o WIF será capaz de modificar o documento WSDL que descreve o serviço, também incluindo o local deste autenticador que o teu serviço confia. Com isso, ao referenciar o serviço em uma aplicação cliente, ela já saberá qual o endereço correto do autenticador e, consequentemente, antes de efetuar a requisição efetiva para uma das operações do serviço, ele será inteligente o bastante para autenticar o usuário no autenticador e, finalmente, invocar a operação solicitada. Através da imagem abaixo, podemos visualizar as etapas deste processo:



É importante dizer que no ambiente ativo, o processo acaba sendo mais rápido quando comparado com o ambiente passivo, pois o cliente não precisa visitar o serviço para saber qual é o autenticador que o mesmo utiliza. Isso já aconteceu durante a referência do serviço na aplicação cliente, que traz, além da descrição do serviço em si, informações inerentes ao autenticador em qual o serviço confia, assim como já foi comentado acima.

Depois de entendermos o processo em um nível macro, precisamos nos atentar em como podemos proceder para preparar tudo isso. Em primeiro lugar, vamos avaliar as opções de template de projeto voltadas para o WCF, que o WIF nos fornece quando instalamos o seu SDK:

  • 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.
  • 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.

Apesar delas existirem, não vamos utilizá-las no decorrer deste artigo. A ideia aqui é apresentar os detalhes de criação do começo ao fim, analisando cada uma das seções necessárias para fazer com que todo esse processo funcione da forma correta. Com isso, teremos três elementos envolvidos no nosso exemplo: o serviço (relying party) que contém todas as funcionalidades que desejamos expor aos clientes, um autenticador (STS), que será o responsável por autenticar os usuários e, finalmente, a aplicação cliente (subjects), que utilizará o serviço WCF para extrair as informações e exibí-las na tela, mas antes disso, deverá autenticar o usuário que a acessa no STS correspondente.

A criação do STS

Para iniciar, vamos começar com a construção do STS, que será o nosso autenticador. Para isso, vamos recorrer à uma aplicação do tipo Console, que irá expor um serviço que aceitará as requisições de login, validará o usuário em algum repositório e, finalmente, devolverá para a aplicação solicitante o token gerado por ele. Como já comentei nas entrelinhas, o STS nada mais é que um serviço, que se utiliza da mesma estrutura de um serviço WCF, mas expondo um tipo de serviço diferenciado, que tem a finalidade de validar e autenticar um usuário. Para termos acesso as classes que nos permite criar este tipo de serviço, devemos referenciar na aplicação o assembly Microsoft.IdentityModel.dll, que é fornecido pelo WIF.

Se o STS nada mais é que um serviço, temos que ter uma classe que o representa, e para isso o WIF já fornece uma classe abstrata chamada SecurityTokenService, que é o nosso STS em si, fornecendo toda a estrutura necessária para a criação de um STS customizado, mas para isso, devemos sobrescrever dois métodos 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ão utilizadas, 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 repositório 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.

Como mencionei, o método GetScope é responsável por capturar os recursos necessários para estabelecer a comunicação segura. Aqui teremos dois certificados, sendo um deles utilizado pelo autenticador (STS) e outro pelo serviço (relying party). Cada um deles vai garantir que as mensagens trocadas não sejam manipuladas por ninguém. Para isso, a classe Scope fornece duas propriedades: EncryptingCredentials e SigningCredentials. Na primeira propriedade, definimos o certificado fornecido pelo serviço, que com a sua chave pública, faremos a criptografia da mensagem quando formos retornar a resposta do processo de autenticação (RSTR). Já a propriedade SigningCredentials, apontamos para o certificado do autenticador, que utilizará para garantir que a mensagem não será alterada durante a sua viagem. Abaixo temos um trecho da classe que representa o serviço de STS, omitindo algumas seções por questões de espaço:

public class ServicoDeAutenticacao : SecurityTokenService
{
    public ServicoDeAutenticacao(ConfiguradorDoServicoDeAutenticacao configurador)
        : base(configurador) { }

    protected override IClaimsIdentity GetOutputClaimsIdentity(
        IClaimsPrincipal principal, RequestSecurityToken request, Scope scope)
    {
        principal.Identities[0].Claims.AddRange(
            RepositorioDeUsuarios.RecuperarClaimsDoUsuario(principal.Identity.Name));

        return principal.Identity as IClaimsIdentity;
    }

    protected override Scope GetScope(IClaimsPrincipal principal, RequestSecurityToken request)
    {
        Scope scope = new Scope(request.AppliesTo.Uri.AbsoluteUri);
        scope.EncryptingCredentials = this.GetCredentialsForAppliesTo(request.AppliesTo);
        scope.SigningCredentials = new X509SigningCredentials(
            CertificateUtil.GetCertificate(
                StoreName.My,
                StoreLocation.LocalMachine,
                "CN=Autenticador"));

        return scope;
    }

    //Outros Membros
}

Depois da classe que representa o serviço de STS criada, o próximo passo é a criação de uma classe que herda da classe SecurityTokenServiceConfiguration. Essa classe permite configurar um serviço de STS, definindo qual será o tipo da classe que representará o STS, que foi aquela que criamos no passo anterior. Essa classe possui várias propriedades, que permite configurar de forma imperativa o STS, mas neste caso, tudo o que precisamos fazer é herdar, e dentro do seu construtor, definir duas principais propriedades: SecurityTokenService e SigningCredentials. A primeira correspondente ao Type da classe que herda de SecurityTokenService, enquanto a segunda, define os dados do certificado usado para a conversação. O código abaixo mostra a sua estrutura:

public class ConfiguradorDoServicoDeAutenticacao : SecurityTokenServiceConfiguration
{
    public ConfiguradorDoServicoDeAutenticacao()
        : base("ServicoDeAutenticacao")
    {
        this.SecurityTokenService = typeof(ServicoDeAutenticacao);
        this.SigningCredentials = new X509SigningCredentials(
            CertificateUtil.GetCertificate(
                StoreName.My,
                StoreLocation.LocalMachine,
                "CN=Autenticador"));
    }
}

Para encerrar as classes customizadas, é necessário a criação de um security token handler. Neste contexto, o handler é responsável por extrair o token enviado pelo cliente, e validá-lo em algum repositório. Como queremos optar para que o usuário informe o seu login e senha, utilizaremos aqui um security token handler exclusivo para este caso, que é o UserNameSecurityTokenHandler.

Ao herdar desta classe, devemos sobrescrever o método ValidateToken, que fornece como parâmetro a instância do token enviado pelo usuário, representado pela classe UserNameSecurityToken, que por sua vez, fornece duas propriedades pertinentes ao modelo de autenticação, que são elas: UserName e Password. O exemplo abaixo ilustra como proceder para customizar essa classe:

public class ValidadorDeUsuario : UserNameSecurityTokenHandler
{
    public override ClaimsIdentityCollection ValidateToken(SecurityToken token)
    {
        UserNameSecurityToken usernameToken = token as UserNameSecurityToken;

        if (!RepositorioDeUsuarios.Validar(usernameToken.UserName, usernameToken.Password))
            throw new SecurityException("Usuário inválido.");

        ClaimsIdentity identidadeAtual = new ClaimsIdentity("ValidadorDeUsuario");
        identidadeAtual.Claims.Add(new Claim(ClaimTypes.Name, usernameToken.UserName));

        return new ClaimsIdentityCollection() { identidadeAtual };
    }

    //Outros Membros
}

Como podemos notar no código acima, é dentro do método ValidateToken que devemos recorrer ao repositório de usuários para validar as credenciais informadas pelo cliente. Caso o usuário não seja encontrado, você pode disparar uma exceção do tipo SecurityException, informando a inexistência daquele usuário.

E para colocar tudo isso em funcionamento, entra em cena a classe WSTrustServiceHost. Essa classe herda diretamente da classe ServiceHost (exposta pela API do WCF), que permite carregar e gerenciar um serviço STS utilizando toda a estrutura do WCF, e com isso, ficará disponível para receber as requisições das aplicações clientes que desejam autenticar seus usuários, antes de efetivamente acessar uma das operações expostas pelo serviço. Em princípio, temos que recorrer a esta classe quando não estamos utilizamos as templates de projetos que vimos acima, pois quando as usamos, isso tudo o que vemos aqui ficará transparente.

Há um overload do construtor da classe WSTrustServiceHost, que aceita como parâmetro uma classe do tipo SecurityTokenServiceConfiguration, contendo toda a configuração necessária de um serviço de STS. O código abaixo ilustra a sua utilização:

using (WSTrustServiceHost host = 
    new WSTrustServiceHost(new ConfiguradorDoServicoDeAutenticacao()))
{
    host.Open();
    Console.WriteLine("[ Serviço de Autenticação em funcionamento ]");
    Console.ReadLine();
}

Assim como qualquer serviço WCF, somente essa configuração não é suficiente para subir o serviço de STS. Ainda há a necessidade de apontar, via arquivo de configuração, outras informações necessárias para o mesmo, tais como endpoints, contratos, baseAddresses, etc., que como sabemos, é necessário para todo e qualquer tipo de serviço WCF. Mas como estamos criando uma espécie de serviço voltado exclusivamente para o STS, temos já contratos predefinidos, que expõem funcionalidades de emissão, cancelamento e renovação de tokens, mas tudo isso tudo será utilizado, em um primeiro momento, internamente pelo runtime do WIF.

Sendo assim, a classe que representará o serviço é a WSTrustServiceContract, que implementa a interface IWSTrust13SyncContract, que define o contrato do serviço, fornecendo as operações comentadas no parágrafo anterior. Agora o nosso serviço deverá expor dois endpoints, sendo um deles utilizando o contrato IWSTrust13SyncContract em conjunto com o binding WS2007HttpBinding, e além disso, um binding tradicional de metadados (IMetadataExchange), que irá expor todas as funcionalidades do serviço de STS. A única configuração relevante do binding, é que devemos definir o atributo clientCredentialType do elemento message como sendo UserName, já que a ideia é permitir ao usuário informar os dados de acesso através da aplicação cliente. Abaixo podemos visualizar as configurações deste serviço:

<?xml version="1.0"?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceContract"
               behaviorConfiguration="serviceConfig">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:9010/sts"/>
          </baseAddresses>
        </host>
        <endpoint address=""
                  binding="ws2007HttpBinding"
                  bindingConfiguration="bindingConfig"
                  contract="Microsoft.IdentityModel.Protocols.WSTrust.IWSTrust13SyncContract" />
        <endpoint address="mex"
                  binding="mexHttpBinding"
                  contract="IMetadataExchange" />
      </service>
    </services>
    <bindings>
      <ws2007HttpBinding>
        <binding name="bindingConfig">
          <security mode="Message">
            <message clientCredentialType="UserName"
                     negotiateServiceCredential="false"
                     establishSecurityContext="false"/>
          </security>
        </binding>
      </ws2007HttpBinding>
    </bindings>
</configuration>

Como se não bastasse, ainda precisamos das configurações referentes ao WIF neste mesmo arquivo de configuração da aplicação que servirá como host do serviço de STS. Como já vimos em artigos anteriores, a seção para essa configuração é a microsoft.identityModel, que deve ser registrada primeiramente antes de ser utilizada.

Depois da seção registrada, o próximo passo é remover o security token handler que é adicionado por padrão (WindowsUserNameSecurityTokenHandler), e em seguida, adicionarmos aquele que criamos acima. Para isso, recorremos à uma coleção chamada securityTokenHandlers, e especificamos ali o tipo completo, incluindo o nome e o assembly onde ele foi criado. Além disso, ainda há a seção serviceCertificate, que apontamos o certificado que o autenticador irá utilizar para se comunicar com o serviço, protegendo todas mensagens que são trocadas entre eles. O código abaixo resume todas as configurações do WIF que são necessárias na aplicação que hospedará o serviço STS, que compõem o arquivo de configuração que vimos acima com as configurações pertinentes ao WCF:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="microsoft.identityModel"
             type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, ..."/>
  </configSections>
  <microsoft.identityModel>
    <service>
      <securityTokenHandlers>
        <remove type="Microsoft.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler, ..." />
        <add type="Autenticador.ValidadorDeUsuario, Autenticador ..." />
      </securityTokenHandlers>
      <serviceCertificate>
        <certificateReference storeLocation="LocalMachine"
                              storeName="My"
                              x509FindType="FindBySubjectDistinguishedName"
                              findValue="CN=Autenticador" />
      </serviceCertificate>
    </service>
  </microsoft.identityModel>
</configuration>

A criação do Serviço

Depois do serviço de autenticação criado, vamos partir para o serviço que utilizará e confiará naquele STS para autenticar os usuários. O serviço que vamos utilizar como exemplo é bem simples, com apenas duas operações que recebem e retornam tipos simples. A ideia aqui será restringir o acesso às operações deste serviço, permitindo o acesso somente depois do usuário ser devidamente autenticado pelo serviço de STS que criamos acima.

Sendo assim, a configuração do serviço WCF com as nossas regras de negócio é simples, contendo os endpoints, os behaviors para expor os metadados e a criação de um hosting, que utilizaremos neste exemplo, uma aplicação do tipo Console. A única diferença aqui na configuração do serviço é a utilização do binding WS2007FederationHttpBinding, que é utilizado quando você está terceirizando o processo de autenticação.

No código abaixo podemos visualizar a configuração do binding do endpoint do nosso serviço. Definimos a segurança como sendo Message, que garantirá que a mensagem seja trafegada de forma segura, mesmo que o transporte não forneça esse recurso. Nas configurações internas, temos um elemento chamado issuerMetadata, que através do atributo address, devemos especificar o endereço do endpoint do serviço de STS, responsável pela autenticação. Em segundo lugar e não menos importante, temos o elemento claimTypeRequirements, que é uma coleção onde podemos especificar quais são as claims que o serviço está exigindo que o STS emita.

<bindings>
  <ws2007FederationHttpBinding>
    <binding name="bindingConfig">
      <security mode="Message">
        <message>
          <issuerMetadata address="http://localhost:9010/sts/mex" />
          <claimTypeRequirements>
            <add claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/name"
                 isOptional="false" />
            <add claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
                 isOptional="false" />
          </claimTypeRequirements>
        </message>
      </security>
    </binding>
  </ws2007FederationHttpBinding>
</bindings>

No serviço também precisamos de algumas configurações pertinentes ao WIF, e como já era de se esperar, é necessário também registrar a seção do mesmo, através do elemento configSections. Neste caso, precisamos definir a audienceUri, que nos permite elencar uma coleção de URIs, que definem os possíveis endereços para qual o token poderá ser entregue, evitando que clientes maliciosos façam uso do mesmo. Em seguida temos a seção serviceCertificate, que apontamos o certificado que o serviço vai utilizar para se comunicar com o autenticador, protegendo as mensagens que são trocadas entre eles. Abaixo o código que ilustra essas configurações:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="microsoft.identityModel"
             type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection ..."/>
  </configSections>
  <microsoft.identityModel>
    <service>
      <issuerNameRegistry type="Servico.ValidadorDoAutenticador, Servico ..." />
      <audienceUris>
        <add value="http://localhost:9000/ServicoDeCredito" />
      </audienceUris>
      <serviceCertificate>
        <certificateReference storeLocation="LocalMachine"
                              storeName="My"
                              x509FindType="FindBySubjectDistinguishedName"
                              findValue="CN=Servico" />
      </serviceCertificate>
    </service>
  </microsoft.identityModel>
</configuration>

Finalmente, ainda analisando o código acima, temos um elemento chamado issuerNameRegistry, que como o próprio nome diz, permite especificarmos ali os STSs em que a aplicação confia. Para termos o controle desta validação, podemos criar uma classe que herde da classe abstrata IssuerNameRegistry, sobrescrevendo o método GetIssuerName, analisando o certificado fornecido pelo autenticador, e assim tomarmos alguma decisão para saber se confiamos ou não nele. Se iremos confiar, então devemos retornar uma string representando o autenticador, caso contrário, devemos disparar uma exceção para reportar essa "falha". O código abaixo ilustra essa classe:

public class ValidadorDoAutenticador : IssuerNameRegistry
{
    public override string GetIssuerName(SecurityToken securityToken)
    {
        X509SecurityToken certificado = securityToken as X509SecurityToken;

        if (certificado != null)
            if (certificado.Certificate.SubjectName.Name == "CN=Autenticador")
                return certificado.Certificate.SubjectName.Name;

        throw new SecurityTokenException("Autenticador Inválido.");
    }
}

O último detalhe é a criação do host, que quando utilizamos um self-hosted, recorrermos à classe ServiceHost, e aqui não será diferente. Só há uma pequena configuração a ser realizada aqui, que é a integração do runtime do WIF ao pipeline do WCF. Essa configuração pode ser feita de forma imperativa ou declarativa. Utilizando a primeira opção, antes de abrir o host, devemos enviar a instância da classe ServiceHost para o método estático ConfigureServiceHost, exposto pela classe FederatedServiceCredentials, assim como podemos notar no trecho de código abaixo:

using (ServiceHost host = new ServiceHost(typeof(ServicoDeCredito), new Uri[] { }))
{
    FederatedServiceCredentials.ConfigureServiceHost(host);

    host.Open();
    Console.WriteLine("[ Serviço de Crédito em funcionamento ]");
    Console.ReadLine();
}

Ao submeter o host para o método ConfigureServiceHost, uma série de elementos são adicionados ao processamento das requisições que chegam para o serviço, efetuando toda a comunicação necessária com o serviço de STS em qual o serviço confia, e além disso, é capaz de capturar o resultado do processo de autenticação realizado remotamente, e com isso prosseguir com o processo de autorização, que determinará o que o usuário poderá ou não acessar.

A autorização continua sendo de responsabilidade do serviço local, que na maioria das vezes recorre às roles para refinar o acesso. Quando estamos trabalhando com o WIF, todas as informações que ele emite para um determinado usuário autenticado, é representado por uma claim. No nosso exemplo, o STS está gerando três claims, sendo uma delas do tipo name, e as outras duas do tipo role. O WCF em conjunto com o WIF são capazes de analisar as claims geradas pelo STS, e depois disso, configurar a propriedade CurrentPrincipal da classe Thread com uma instância da classe ClaimsPrincipal, representando a identidade do usuário e suas respectivas claims.

Desta forma, podemos continuar trabalhando com o modelo de programação da mesma forma que já utilizávamos no passado, ou seja, através do método IsInRole (imperativo) ou com o uso do atributo PrincipalPermissionAttribute (declarativo), assim como é exemplificado abaixo:

public class ServicoDeCredito : IConsultasFinanceiras
{
    [PrincipalPermission(SecurityAction.Demand, Role = "Funcionario")]
    public decimal RecuperarLimiteDeCredito(int codigoDoCliente)
    {
        //Implementação
    }

    [PrincipalPermission(SecurityAction.Demand, Role = "Gerente")]
    public void DefinirNovoLimiteDeCredito(int codigoDoCliente, decimal novoValor)
    {
        //Implementação
    }
}

A criação do Cliente

Finalmente, depois dos dois serviços devidamente implementados, chega o momento de consumí-los em uma aplicação cliente. Tudo o que precisamos fazer nela, é adicionar a referência para o serviço WCF, assim como já fazemos. Não há necessidade de fazer referência ao serviço de STS na aplicação cliente, pois além do documento WSDL descrever as operações do serviço, ele também traz todas as informações necessárias a respeito do serviço responsável pela autenticação, como o endereço onde ele está localizado, quais são as claims que o serviço exige que o autenticador emita, etc.

Ao referenciar o serviço através da opção Add Service Reference da IDE do Visual Studio ou através do utilitário svcutil.exe, o proxy gerado conterá as informações tradicionais do endpoint do serviço com o qual a aplicação quer se comunicar, mas também incluirá algumas configurações extras, que determinam como a comunicação com o STS será realizada. Justamente por conhecer onde o STS está e como devemos ser comunicar com ele, que o ambiente ativo é bem mais rápido que o passivo, como já discutido anteriormente.

Outro detalhe importante no arquivo de configuração, são as chaves públicas dos certificados envolvidos no processo. Uma delas foi fornecida pelo serviço e a outra pelo autenticador, que o runtime do WCF e do WIF irão utilizá-las durante o processo de comunicação para garantir com que as mensagens que serão trocadas entre as partes sejam protegidas.

Por fim, a aplicação cliente só precisa consumir o serviço, apenas se atentando a informar o login e senha antes de invocar alguma das operações. O código abaixo ilustra a utilização do serviço, através do proxy que foi criado durante a referência do serviço. O arquivo de configuração do cliente será omitido aqui por questões de espaço.

using (ConsultasFinanceirasClient proxy = new ConsultasFinanceirasClient())
{
    proxy.ClientCredentials.UserName.UserName = "Israel";
    proxy.ClientCredentials.UserName.Password = "P@ssw0rd";

    proxy.DefinirNovoLimiteDeCredito(560, 10000);

    Console.WriteLine("Limite para o cliente 200: {0:C2}",
        proxy.RecuperarLimiteDeCredito(200));
}

Conclusão: Depois de tudo o que fizemos aqui, o nosso serviço ficou completamente independente de como se faz a autenticação do usuário. Se tivermos um barramento de serviços dentro de algum local, como uma empresa, todos os serviços podem confiar em um único autenticador, e a reutilização será muito grande, já que temos todo o processo centralizado, facilitando a manutenção, já que os serviços consumidores nada sabem sobre a forma como estamos autenticando os usuários.

TesteComAmbienteAtivo.zip (42.08 kb)

Tags: , , ,

Security | WCF | WIF

QCon - Como unificamos nossa camada de autenticação?

by Israel Aece 25. May 2010 22:04

O principal evento de arquitetos e desenvolvedores chega a América Latina. O QCon SP traz, dias 11 e 12 de Setembro, ícones internacionais e nacionais de diversas áreas, com apresentações de alto nível técnico. Com sistemas cada vez mais complexos, o QCon aborda não apenas uma única tecnologia ou aspecto: passa de Java, .NET e Rails até Arquitetura, Design, Cloud, Escalabilidade, Replicação, Cache e casos de sucesso.

Neste evento, eu fui convidado para compor juntamente com alguns ícones do mundo .NET (tais como Thiago Cruz Soares e Fabio Galuppo), o grupo de palestrantes focados em tecnologias Microsoft/.NET. A minha palestra tem como finalidade abordar o WIF - Windows Identity Foundation, que já falei bastante por aqui. Abaixo temos a descrição completa da palestra:

Título: Como unificamos nossa camada de autenticação?
Palestrante: Israel Aece
Descritivo: Autenticação e autorização são dois conceitos importantes que existem no desenvolvimento de software. A autenticação é a necessidade de saber quem o usuário é, enquanto a autorização é saber que direitos ele tem no software. Há uma grande complexidade em torno destes temas, pois cada aplicação precisa se preocupar em vários detalhes para garantir a segurança, tais como: um local seguro para armazenar as credenciais de acesso, definição de políticas para a criação e reutilização de senhas, etc. E como se isso não fosse o bastante, corremos um grande risco de termos a mesma infraestrutura repetida entre várias aplicações, dificultando a manutenção, segurança e a experiência de navegação dos usuários que as acessam. Essa palestra mostrará cenários típicos do nosso dia-à-dia, como SSO (Single Sign-On) e federação dentro da plataforma .NET.

Tags: ,

General | Security | WIF

A importância do StrongName

by Israel Aece 11. May 2010 22:28

Imagine que você precise criar um conjunto de componentes que gerenciam a área de recursos humanos de uma determinada empresa. Entre esses componentes, vamos ter um chamado Salario, que dado o nome do funcionário, valor base do salário e a quantidade de horas extras que o funcionário realizou no mês, retorná o valor líquido que a empresa deverá pagar a ele.

Para ilustrar isso, temos um projeto do tipo Class Library, chamado de RegrasDeNegocio. Dentro deste projeto, criamos uma classe chamada Salario, que possuirá apenas o método chamado de Calcular, que receberá os parâmetros mencionados acima. Traduzindo tudo isso em código, teremos algo como:

public class Salario
{
    public decimal Calcular(string nomeDoFuncionario, decimal valorBase, decimal horasExtras)
    {
        return valorBase + horasExtras;
    }
}

Note que não há nenhuma complexidade envolvida para não desviarmos o foco. Como sabemos, este projeto dará origem à uma DLL, que poderá ser utilizada por várias aplicações que rodam dentro da empresa. Como já era de se esperar, vamos referenciá-la em um projeto chamado AplicacaoDeRH, que utilizará esse componente recém criado para calcular os salários de seus respectivos funcionários. Com isso, o código de consumo na aplicação final é algo parecido com isso:

Console.WriteLine(new Salario().Calcular("Israel Aece", 1000.00M, 250.00M));

Dá mesma forma que antes, vamos manter a simplicidade aqui. Depois deste software (EXE + DLL) instalado na máquina do responsável pelo RH da empresa, eu chego até o diretório físico onde ele está instalado. Copio esses dois arquivos para minha máquina para começar a analisar como esse componente e aplicação foram desenvolvidos. Como sabemos, dentro de qualquer assembly .NET, temos apenas código IL, que é um código ainda decompilável. Podemos utilizar o Reflector para isso e, consequentemente, visualizar tudo o que eles possuem (classes, métodos, parâmetros, etc.).

A partir de agora, vamos explorar a vulnerabilidade. Como eu conheço toda a estrutura que o componente (DLL) tem, nada impede de eu criar um projeto no Visual Studio .NET, com o mesmo nome, com os mesmos tipos e os mesmos parâmetros. Com isso, eu vou manipular o corpo do método Calcular, verificando se o funcionário que está sendo calculado o salário sou eu, e se for, multiplico a quantidade de horas extras por 2, para que eu possa ganhar um valor maior do que realmente deveria.

public class Salario
{
    public decimal Calcular(string nomeDoFuncionario, decimal valorBase, decimal horasExtras)
    {
        if (nomeDoFuncionario == "Israel Aece")
            horasExtras *= 2;

        return valorBase + horasExtras;
    }
}

Depois de compilado, isso dará origem à uma - nova - DLL com o mesmo nome. De alguma forma, eu chego até o computador do responsável pelo RH da empresa, e substituo fisicamente a DLL anterior por essa DLL que acabamos de desenvolver, e que viola a regra de negócio, pois manipula o resultado para beneficiar um funcionário específico. Com isso, quando o responsável pelo RH for calcular o meu salário, ele me pagará duas vezes o valor das minhas horas extras. Se ele confia cegamente no software que dá o resultado, estou sendo beneficiado, a empresa prejudicada e dificilmente alguém encontrará o problema.

A importância do StrongName

Todo e qualquer assembly possui algumas características que ajudam ao runtime determinar a sua identidade. A identidade de qualquer assembly .NET é composta por quatro informações, a saber: nome, versão, cultura e uma chave pública. O nome nada mais é que o nome do assembly, desconsiderando a sua extensão. A versão é o número em que o projeto se encontra, e que por padrão é 1.0. Já a cultura determina se o assembly é sensitivo à alguma cultura e, finalmente, a chave pública, qual falaremos mais adiante.

Podemos utilizar várias ferramentas para conseguir visualizar essas informações. No nosso caso, se abrirmos dentro do Reflector o componente que criamos inicialmente, aquele que possui o código legal, poderemos comprovar essas características que ajudam a identificar o assembly:

RegrasDeNegocio, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Quando criamos a assembly ilegal, com o código que multiplica a quantidade de horas extras por dois, poderemos notar que o resultado será idêntico, ou seja, não há nada que diferencie a identidade dos dois assemblies. Sendo assim, a substituição física é o suficiente para comprometer a regra de cálculo de salário, já que para a aplicação que a consome, acaba sendo o mesmo componente. Se repararmos melhor, entre as quatro informações que compõem a identidade do assembly, uma delas é chamada de PublicKeyToken, e que está nula (não definida). É justamente nela que está a solução para o nosso problema.

Dentro do .NET Framework existem dois tipos de assemblies: os que são fracamente nomeados e os que são fortemente nomeados. Aqueles que são fracamente nomeados (que é o padrão para todos os projetos .NET), são estruturalmente idênticos, possuindo o mesmo formato e os mesmos tipos definidos, mas como está, dá margem para esse tipo de problema.

Isso acontece porque quando efetuamos a referência do componente RegrasDeNegocio.dll na aplicação AplicacaoDeRH.exe, o .NET injeta no manifesto da AplicacaoDeRH, informações pertinentes ao assembly de regras de negócio. Se analisarmos o manifesto do EXE, veremos a seguinte informação:

Na imagem acima, podemos verificar que dentro da AplicacaoDeRH há uma referência para o componente RegrasDeNegocio, que inclui o nome e a versão dele. Como até então o componente ilegal possui a mesma identidade, a aplicação consome ele sem maiores problemas, não sabendo que se trata de um componente malicioso.

Para resolver o nosso problema, vamos recorrer aos assemblies fortemente nomeados (strong names). A única e principal diferença em relações aos assemblies fracamente nomeados, é a atribuição de uma chave única, que não se repetirá em nenhum lugar do mundo. A Microsoft criou esse recurso, desde a primeira versão do .NET Framework, que utiliza um par de chaves (pública e privada) para garantir a unicidade do assembly. Antes de ver como elas funcionam, vamos primeiramente entender como gerá-la. O .NET Framework fornece uma ferramenta chamada SN.exe, que é responsável por gerar e manipular essas chaves. Para gerar a nossa chave, podemos executar o seguinte comando (através do prompt do Visual Studio .NET):

SN -k MinhasChaves.snk

O arquivo gerado possui as duas chaves, a pública e a privada e você pode nomear o arquivo da forma que quiser. Com o par de chaves gerado, podemos criar um segundo arquivo para acomodar somente a chave pública, mas em um primeiro momento, isso é desnecessário. A ideia aqui é somente visualizarmos o que temos como chave pública:

SN -p MinhasChaves.snk MinhaChavePublica.snk

Para visualizarmos a chave pública, podemos utilizar o seguinte comando:

SN -tp MinhaChavePublica.snk

Public key is
0024000004800000940000000602000000240000525341310004000001000100f1589e575d9c20
cc36a0fb7245d74c8d69ddc26a0c92ebee5e65dba7c94a6583701176cc5a8fd795e11d7e366c49
a19f3ae28509fa8961e6eca103353fe98168a402dc35001b98d9d5325f6121bde11bc698f268a3
e7e338b950b565be26e371c2550dfaee54f9ef8993dc476f60b2ab5ad69d5ae832ddd7e35e43ad
6daafae2

Public key token is 0b8510fcd7fd739a

Só que o arquivo snk por si só não funciona. Você precisa vinculá-lo ao assembly qual deseja assinar. Para isso, você pode abrir o arquivo AssemblyInfo.cs, e adicionar o atributo AssemblyKeyFileAttribute, que recebe o caminho físico até o arquivo que contém o par de chaves, e que no nosso exemplo é MinhasChaves.snk.

[assembly: AssemblyKeyFile(@"C:\MinhasChaves.snk")]

Ao compilar o assembly com a chave vinculada, veremos que a identidade do assembly já mudará. Ao vincular a chave criada acima no nosso componente RegrasDeNegocio, a identidade irá aparecer da seguinte forma:

RegrasDeNegocio, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0b8510fcd7fd739a

A única e essencial diferença é que agora a propriedade PublicKeyToken reflete exatamente a nossa chave pública, que está contida no arquivo MinhasChaves.snk vinculado ao componente. A partir de agora, as aplicações que referenciarem o componente RegrasDeNegocio, guardarão além do nome e versão do mesmo, a chave pública que o identifica. Depois dessas alterações, se visualizarmos o manifesto da aplicação AplicacaoDeRH, teremos o seguinte resultado:

Com isso, qualquer pessoa maliciosa que tente refazer o assembly, por mais que ela se atente a criar toda a estrutura de tipos e métodos, definir o mesmo nome de assembly, e ainda, assinar com um outro strong name, ela jamais conseguirá reproduzir a mesma identidade e, consequentemente, não conseguirá mais alterar o componente que está instalado no cliente. É importante dizer que fisicamente, a substituição ainda poderá ocorrer, mas quando a aplicacação AplicacaoDeRH tentar acessar algum recurso do assembly RegrasDeNegocio, uma exceção será disparada, informando que o assembly solicitado não corresponde aquele que foi inicialmente referenciado.

Observação: Toda essa segurança pode continuar vulnerável se você deixar o arquivo com a chave privada em mãos erradas. Se a pessoa maliciosa conseguir ter acesso a esse arquivo, ela irá gerar o assembly idêntico como ela já fazia, mas ao invés de criar um novo par de chaves para assinar o assembly, ela utilizará o mesmo que você utilizou para assinar o seu, que é o verdadeiro, e com isso todo o problema volta a acontecer.

Para finalizar, vamos entender como todo esse mecanismo funciona e como o runtime do .NET Framework assegura isso. Quando geramos a chave a partir do utilitário SN.exe, um par de chaves é adicionado no arquivo MinhasChaves.snk. Quando compilamos o projeto com esse arquivo vinculado a ele, o .NET gera um hash do componente utilizando o algoritmo SHA1 e assina esse hash com a chave privada. O resultado deste processo é adicionado no próprio assembly, incluindo também a sua chave pública, que está matematicamente relacionada à chave privada. A imagem abaixo ilustra esse processo:

Como vimos acima, quando o componente é referenciado na aplicação que o utiliza, a chave pública também é adicionada à aplicação. Durante a execução, o .NET Framework irá aplicar o mesmo algoritmo de hash no conteúdo do componente (DLL) e dará origem à um novo hash e a chave pública embutida na aplicação que consome aquele componente, será utilizada para extrair o conteúdo (já "hasheado") que está embutido na DLL do componente. Para determinar se a DLL é a mesma ou não, o resultado do hash deve ser igual, do contrário, a DLL foi substituída e, felizmente, uma exceção será disparada, evitando assim de consumir um componente ilegal. A imagem abaixo ilustra esse processo:

Conclusão: Ao assinar uma aplicação/componente com um strong name, podemos tirar proveito de várias funcionalidades, como por exemplo, o refinamento de segurança, a instalação no GAC, que por sua vez possibilita a centralização, execução lado a lado de múltiplas versões de um mesmo componente, etc. Mas um dos principais benefícios fornecidos por ele, é a unicidade do componente, evitando que alguém consiga reproduzí-lo e, consequentemente, colocar em risco a execução e a confiabilidade das aplicações que a consomem.

Tags:

.NET Framework | Security

Role vs. Claims

by Israel Aece 19. April 2010 22:59

Quando estamos lidando com a autorização dentro de uma aplicação, é muito comum configurarmos o acesso às seções dentro do mesmo de acordo com o papel (role) em que o usuário está contido. Muitas vezes o papel reflete o departamento ou grupo em que o usuário encontra-se cadastrado dentro da empresa, independentemente se esse controle é realizado através da própria aplicação ou através de algum centralizador, como é o caso do Active Directory (AD).

Mas ao trabalhar com autorização baseada em claims (afirmações), você não está restrito a isso. A autorização pode estar baseada em outras informações, que vão muito além de simples strings. Podemos, por exemplo, conceder o acesso somente aqueles usuários que são maiores de 18 anos, e para isso vamos nos basear na data de nascimento dele.

Ao criar uma claim, devemos especificar o seu tipo (String, Integer, DateTime, Double, etc) e o seu respectivo valor. Além disso, temos a espécie da claim, que podem ser valores predefinidos pelo WIF, e que determinará qual é a finalidade dela, como por exemplo: name, email, role, etc. Como podemos reparar, um papel nada mais é que uma claim, que é inserida dentro do token emitido pelo STS.

Para manter a compatibilidade com a estrutura de autorização do .NET, continuamos recorrendo à tradicional interface IPrincipal, ou melhor, à uma de suas derivações, que é a IClaimsPrincipal. Ao interrogar o método IsInRole, que dado um papel (role), retornará um valor boleano indicando se o usuário está ou não contido nele. Quando você utiliza esse método, o WIF procura dentro da coleção de claims emitidas pelo STS, alguma que corresponda ao papel. Por padrão, ele tenta procurar pela claim com o nome de "role", mas você pode alterar esse comportamento. Se por algum motivo, a claim que representa o papel é uma outra claim com um outro nome, você pode alterar isso através da propriedade RoleClaimType da interface IClaimsIdentity.

Já se você precisar refinar a sua autorização através de alguma outra claim que não seja o papel do usuário, você pode recorrer a coleção de claims, que também é exposta através da propriedade Claims da interface IClaimsIdentity. Por exemplo, se o STS emitir uma claim que corresponde à data de nascimento do usuário, e quisermos somente conceder o acesso se ele for maior que 18 anos, então podemos utilizar o seguinte código:

IClaimsIdentity identity = Thread.CurrentPrincipal.Identity as IClaimsIdentity;

if (identity != null)
{
    string data =
        (
            fromin claimsIdentity.Claims
            where c.ClaimType == ClaimTypes.DateOfBirth
            select c.Value
        ).SingleOrDefault();

    if (!string.IsNullOrWhiteSpace(data))
    {
        const int IDADE_PERMITIDA = 18;
        DateTime dataDeNascimento = DateTime.MinValue;

        if (DateTime.TryParse(data, out dataDeNascimento))
        {
            this.EntrarNoSite.Enabled =
                (DateTime.Now.Subtract(dataDeNascimento).Days / 365) > IDADE_PERMITIDA;
        }
    }
}

Conclusão: O STS pode emitir claims que descrevem o usuário que ele conhece, sendo essas claims informações que a aplicação pode utilizar como quiser para poder identificar ou refinar a autorização daquele usuário, e como disse anteriormente, nem sempre utilizaremos roles para customizar o acesso. Finalmente, podemos concluir que todo papel (role) é uma afirmação (claim), mas nem toda afirmação é um papel.

Tags: ,

Security | 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

Explorando o WIF

by Israel Aece 19. February 2010 22:46

O novo modelo de autenticação e autorização possui três elementos, conhecidos como: Identity Providers, Relying Parties e Subjects. Já falamos sobre o papel que cada um deles exerce dentro deste novo sistema em um artigo anterior. Mas como utilizar estes elementos dentro das nossas aplicações, ou melhor, como fazer com que uma aplicação seja candidata à receber claims emitidas por algum identity provider?

É justamente neste momento que entra em cena um dos grandes pilares que compõem o novo modelo de autenticação e autorização, que é o Windows Identity Foundation (WIF). Através deste novo framework, a Microsoft disponibiliza uma série de tipos que podem ser utilizados não somente para construir aplicações que receberão as claims (relying parties), mas também para estender alguns recursos existentes, como por exemplo, tipos para a criação de um STS (Security Token Service) customizado, criar extensões para o ADFS 2.0, entre outras coisas.

O WIF expõe uma série de tipos para a criação das relying parties, e o uso de cada um destes tipos está condicionado ao tipo de ambiente que está sendo aplicado, que pode ser passivo ou ativo. A finalidade deste artigo é abordar os tipos comuns, e que são úteis para qualquer um destes ambientes. Tipos específicos, utilizados pelos ambientes passivos/ativos e também para estensibilidade, serão abordados em futuros artigos.

Antes de falar diretamente das classes do WIF, é importante dizer que desde a primeira versão do WCF, já havia um assembly chamado System.IdentityModel.dll, que fornece classes para construir serviços que suportem claims. O problema é que muitas outras classes que também são necessárias para executar algumas outras tarefas não eram públicas, tornando um desafio para aqueles que estão precisavam disso. E para complicar ainda mais, não havia um modelo de programação uniforme, que poderíamos facilmente acoplar à execução das aplicações, pois não mantém compatibilidade com o formato tradicional de segurança imposto pelo .NET Framework. O papel do WIF é unificar esse modelo de autenticação e autorização, tornando algo comum para qualquer tipo de aplicação que você desenvolva.

Desde as primeiras versões do .NET Framework, há duas interfaces que controlam a autenticação e autorização dos usuários: IIdentity e IPrincipal. A primeira é reponsável por representar o usuário que está autenticado, fornecendo entre outras informações, o seu nome. Já a interface IPrincipal, representa o contexto de segurança daquele usuário, e fornece uma propriedade chamada Identity do tipo IIdentity e um método chamado IsInRole, que dado uma string representando um papel, retorna um valor boleano indicando se o usuário está ou não contido nele.

E onde elas estão dentro da aplicação? Dentro do namespace System.Threading existe uma classe chamada Thread. Essa classe determina como controlar uma thread dentro da aplicação, e que entre vários membros, possui uma propriedade chamada CurrentPrincipal que recebe a instância de um objeto que implementa a interface IPrincipal. É através desta propriedade que devemos definir qual será a identity e a principal que irá representar o contexto de segurança para a thread atual. Para maiores detalhes, consulte o capítulo 9 deste livro.

Depois de uma pequena recapitulação, vamos começar a abordar o que temos à disposição dentro do WIF. As classes que veremos a seguir, estão dentro do assembly Microsoft.IdentityModel.dll e debaixo do namespace Microsoft.IdentityModel.Claims. Como uma das preocupações da Microsoft foi manter compatibilidade com o modelo de segurança do .NET Framework, ela criou versões específicas das interfaces IIdentity e IPrincipal, que são chamadas de IClaimsIdentity e IClaimsPrincipal. Abaixo temos a imagem do diagrama que representa a hierarquia entre as interfaces de identity:

A primeira propriedade que ela fornece, chamada de Actor, é do tipo IClaimsIdentity, que é útil em cenários de delegação, e em conjunto com ela, temos a propriedade BootstrapToken, mas veremos mais detalhes sobre elas em um artigo futuro, que abordará exatamente este cenário. A terceira propriedade, chamada Claims, é uma das mais importantes para este modelo de autenticação/autorização. Esta propriedade expõe uma coleção do tipo ClaimCollection, onde cada elemento é representado por uma instância da classe Claim, que detalheremos mais adiante. Label é uma propriedade de tipo string, que recebe uma informação qualquer, que personaliza aquela identidade.

Como sabemos, a propriedade Name exposta pela interface IIdentity, retorna o nome do usuário atual, enquanto o método IsInRole, faz verificações baseando-se nos papéis daquele mesmo usuário. Como esses membros são popularmente utilizados, como iremos preencher essas informações? É justamente aqui que as propriedades NameClaimType e RoleClaimType são úteis. Como todas as informações emitidas por um identity provider chegam para uma relying party através de claims, é necessário saber qual claim representa o nome do usuário (IIdentity.Name) e o papel (IPrincipal.IsInRole). Geralmente, cada claim nada mais é que uma string, representado por uma URI, mas não que isso seja obrigatório. Abordaremos mais detalhes sobre a sua representação quando falarmos à respeito da classe Claim, ainda neste artigo.

Observação: Como disse anteriormente, claims podem ser muito mais expressivas do que simplesmente ter o nome do usuário e seus papéis. A necessidade de termos as propriedades NameClaimType e RoleClaimType, é justamente para manter compatibilidade com todo o legado de segurança que já conhecemos e/ou temos em aplicações que fazem uso do .NET Framework.

Da mesma forma que vimos acima, a interface IClaimsPrincipal também herda diretamente da interface IPrincipal, incluindo apenas uma única propriedade, chamada de Identities, que é do tipo ClaimsIdentityCollection, ou seja, uma coleção onde cada elemento dentro dela é do tipo IClaimsIdentity. Na maioria das vezes, essa propriedade somente terá um único elemento, representando o usuário atual, entretanto, em cenários mais avançados, pode haver várias identidades de um mesmo usuário, emitidos por orgãos diferentes. A imagem abaixo ilustra a herança entre essas duas interfaces:

Como vimos acima, a interface IClaimsIdentity, expõe uma coleção de claims. Cada elemento é representado por uma instância da classe Claim, e como o próprio nome diz, representa uma claim entre as várias que um identity provider pode emitir para um determinado subject. Como já comentado em artigos anteriores, cada claim é uma espécie de afirmação que um identity provider emite para algum subject, e entre elas, podemos ter o nome do usuário e os papéis em que ele se encontra.

Como podemos notar na imagem acima, a classe Claim fornece várias propriedades que descreve cada uma delas. A propriedade ClaimType, retorna uma string, que na maioria das vezes é uma URI, representando o que aquela claim significa (se é o nome, e-mail, data de nascimento, etc.). A propriedade Issuer também retorna uma string com o nome daquele que emitiu a claim. OriginalIssuer tem sentindo quando trabalhamos em ambientes federados, onde pode haver mais do que um emissor. Properties é uma propriedade que expõe um dicionário de strings, que permite adicionarmos informações extras sobre uma determinada claim. A propriedade Subject é clara, retorna uma instância do tipo IClaimsIdentity que representa o subject ao qual aquela claim pertence. A propriedade Value retorna o valor daquela claim ("Israel Aece", "ia@israelaece.com", 05/09/1981, etc.). E, finalmente, temos a propriedade ClaimValue, que ajuda a definir o tipo daquela claim. Essa propriedade é útil durante o processo de deserialização do token, fornecendo informações a respeito do tipo que aquela claim é. Há uma classe estática chamada ClaimValueTypes, que possui várias constantes públicas que já são conhecidas, tais como: Integer, DateTime, Double, etc.

As interfaces não são "instanciáveis", e justamente por isso, precisamos de classes que implementem esses recursos expostos pelas interfaces apresentadas acima. E para isso, a Microsoft disponibilizou também as classes ClaimsIdentity e ClaimsPrincipal, que implementam as interfaces IClaimsIdentity e IClaimsPrincipal, respectivamente. Além dos membros que elas são obrigadas a implementarem, ainda há na classe ClaimsPrincipal, métodos estáticos, que auxiliam na criação de uma principal deste tipo, extraindo as informações de objetos que já são conhecidos, como HttpContext ou IIdentity. A imagem abaixo ilustra a estrutura destas duas classes:

Essas classes são, na maioria das vezes, instanciadas pelo próprio runtime do WIF, que as cria de acordo com o token que é recebido do identity provider. Depois de criadas, o próprio WIF armazena a instância da classe ClaimsPrincipal dentro da propriedade ThreadPrincipal, exposta pela classe Thread, ou se estivermos falando de uma aplicação ASP.NET, ela - também - estará acessível através da propriedade estática User da classe HttpContext.

Em ambos os casos, essas propriedades recebem e retornam classes que implementam a interface IPrincipal, e graças ao polimorfismo, podemos definir classes que implementam a interface IClaimsPrincipal nestas mesmas propriedades. O único detalhe importante, é que para extrair e ter acesso à todas as propriedades expostas pela interface IClaimsPrincipal, será necessário efetuar a conversão, assim como é mostrado no trecho de código abaixo:

IClaimsPrincipal principal = Thread.CurrentPrincipal as IClaimsPrincipal;
if (principal != null)
{
    IClaimsIdentity identity = (IClaimsIdentity)principal.Identity;

    Console.WriteLine("Nome: {0}", identity.Name);
    Console.WriteLine("Label: {0}", identity.Label);

    foreach (Claim c in principal.Claims)
        Console.WriteLine("{0}: {1}", c.ClaimType, c.Value);
}

Obviamente que essas classes por si só não funcionam. Quem faz grande parte do trabalho para a criação de cada uma delas é o runtime do WIF, e a sua configuração depende diretamente de qual tipo de aplicação estaremos utilizando (web ou serviços). O que vimos no decorrer deste artigo, são as classes que representam um subject para uma relying party, e isso não importa o tipo de aplicação que estamos utilizando. Em futuros artigos, analisaremos como configurar cada uma delas, e vamos notar que essas propriedades já estarão devidamente preenchidas.

Conclusão: Este artigo apresentou os novos tipos que temos a nossa disposição para a criação de aplicações (relying parties), que consomem claims geradas por algum identity provider. Reparamos também que o modelo de programação segue a mesma estrutura imposta pelo .NET Framework, e que por isso, podemos reaproveitar o conhecimento adquirido anteriormente, para ter um impacto bem menor quando migramos para este novo modelo.

Tags: , ,

Security | WIF

Os Elementos do Sistema de Identidade

by Israel Aece 18. February 2010 08:04

No artigo anterior, falamos um pouco sobre os problemas conhecidos quando lidamos com a autenticação e autorização em uma aplicação, e no final dele, falei superficialmente sobre os produtos que a Microsoft tem desenvolvido para tornar a utilização do modelo baseado em claims mais simples. A partir de agora, vamos analisar os elementos que compõem o sistema de identidade, analisando o fluxo e alguns conceitos que circundam este modelo, e que é independente de tecnologia.

Tudo o que veremos a seguir é conhecido como Identity MetaSystem, que consiste em uma infraestrutura que abstrai todas as operações necessárias para promover tudo o que é preciso para suportar identidades em cima da internet, fornecendo uma arquitetura interoperável, que permite as mais variadas plataformas implementar e dar suporte à este modelo. Existem três grandes elementos que fazem parte dele, e que são responsáveis pela propagação das identidades, a saber:

  • Identity Providers: É o responsável pela validação e emissão de tokens, que são fornecidos para alguém, contendo um conjunto de claims.
  • Relying Parties: A aplicação que recebe e usa esses tokens que são emitidos por algum Identity Provider.
  • Subjects: É alguma coisa ou alguém que possui uma identidade digital (e suas claims), e que na maioria das vezes, representará um usuário.

Cada produto desenvolvido pela Microsoft tem como alvo um dos elementos acima, sendo o ADFS 2.0 uma espécie de identity provider; já o Cardspace permite o gerenciamento das identidades de um subject e, finalmente, o WIF ajuda na construção de aplicações que recebem esses tokens. Como dito anteriormente, todos esses elementos seguem padrões de mercado, e que são rigidamente gerenciados por órgãos independentes. Os protocolos que são utilizados para troca de informações foram desenhados para cruzar limites, que antes deles, eram invioláveis (plataforma e firewalls).

Outro elemento importante que faz parte deste modelo é o Security Token Service (STS). Como o próprio nome diz, trata-se de um serviço que está dentro de um identity provider, e que é responsável por emitir e empacotar as claims que são geradas para alguém, seguindo alguns padrões de mercado que analisaremos mais tarde, ainda neste artigo.

Há algumas técnicas que podem ser utilizadas para criar ambientes baseados em claims. Por exemplo, aplicações web e serviços SOAP (WCF) podem ser considerados relying parties, pois podem fazer uso de claims fornecidas por algum identity provider em nome de alguém. Já o navegador (browser) e aplicações Smart Client, são consideradas os subjects, já que são os responsáveis por gerenciar o fluxo do processo de autenticação, que direta ou indiretamente, irão direcionar o usuário para efetuar a sua autenticação no identity provider em que a aplicação confia.

Quando utilizamos este novo modelo, temos dois ambientes, onde cada um deles trabalha de forma ligeiramente diferente. Esses ambientes são conhecidos como ambiente passivo e ativo. A partir daqui, vamos analisar cada um desses ambientes, tentando abordar como esse modelo trata cada um deles, sem abordar as tecnologias que estão envolvidas. Para ilustrar melhor como cada um deles funciona, vamos analisar a imagem abaixo:

No ambiente passivo, o subject é o browser, enquanto a relying party é uma aplicação web. Quando um usuário requisita uma página que está protegida (1), a aplicação faz essa verificação e nota que o usuário não está autenticado. Com isso, a aplicação redireciona o usuário para o identity provider em que ela confia (2). Este, por sua vez, faz a autenticação do usuário. Aqui não importa o modelo de autenticação que é utilizado (Windows, UserNames, certificados, etc.). Depois de devidamente autenticado, o identity provider retorna o token correspondente aquele usuário (3), que a partir de agora, enviará esse token para todas as requisições subsequentes (4). Como a verificação da existência do token acontece em todas as requisições, uma vez que o usuário estiver autenticado, ele não será mais redirecionado para o identity provider.

Já no ambiente ativo, o fluxo muda um pouco. Neste cenário, o subject é uma aplicação Smart Client, enquanto a relying party é um serviço SOAP (WCF). O primeiro passo passa a ser a autenticação no respectivo identity provider (1), que uma vez autenticado, um token é emitido para aquele usuário (2). Com isso, todas as requisições irão embutir em seus respectivos headers, o token deste usuário (3). Como a relying party também conhece e confia naquele mesmo identity provider, então o acesso às operações do serviço será garantido.

É importante dizer que no ambiente ativo, o processo acaba sendo mais rápido quando comparado com o ambiente passivo, pois o cliente não precisa visitar o serviço para saber qual é o identity provider que o mesmo utiliza. Isso já aconteceu durante a referência do serviço na aplicação Smart Client, que traz, além da descrição do serviço em si, informações inerentes ao identity provider em que o serviço confia.

Identity Federation

O que vimos acima consiste nos cenários onde todos os participantes estão dentro de um mesmo domínio. Mas um dos principais cenários que este modelo atende, é justamente quando temos os participantes do sistema separados, em domínios diferentes. Da mesma forma que vimos anteriormente, a aplicação ou serviço também poderá aceitar claims que são emitidas por outros identity providers, que estão além do seu domínio, mas que indiretamente há uma relação de confiança estabelecida entre eles. A possibilidade de integração entre os dois domínios também é conhecida como Identity Federation.

Utilizar esta técnica, facilitará muito a possibilidade de SSO (Single Sign-On), que é a possibilidade de se autenticar uma única vez, e reutilizar aquela mesma credencial por todas as aplicações e serviços (relying parties) que aquele usuário utiliza, mesmo que essas aplicações estejam hospedadas nos servidores dos parceiros da nossa empresa. Outro grande ponto positivo desta opção, é que se o usuário não fizer mais parte da minha empresa, tudo o que eu preciso fazer, é remover/desabilitar a sua respectiva conta no meu domínio e, consequentemente, ele não terá mais acesso aos parceiros, já que os parceiros somente confiam em usuários que possuem os tokens emitidos por mim.

Da mesma forma que vimos acima, "cenários federados" também suportam os ambientes passivo e ativo, com mudanças simples no fluxo das informações. E para clarear como o fluxo ocorrerá, vamos utilizar imagens para ilustrar, começando com o ambiente passivo:

Note que o usuário, que está no domínio 1, tenta acessar uma aplicação web que está hospedada no domínio 2 (1). A aplicação detecta que o usuário ainda não está autenticado, e o redireciona para o identity provider do domínio onde a aplicação está hospedada, que é o orgão que ela confia. Como parte deste redirecionamento, o identity provider do domínio 2 conhece o identity provider do usuário, que está no domínio 1. Isso fará com que o usuário seja novamente redirecionado para o identity provider que o conhece, que é aquele que está debaixo do domínio 1 (2). Ao validar o usuário, o token que o representa dentro da empresa é emitido (3). Depois disso, o token gerado no domínio 1, é encaminhado para o identity provider do domínio 2 (4), que o validará e fará eventuais transformações, criando ou mapeando as claims para claims que a aplicação espera. Isso fará com que um novo token seja emitido (5), e que será utilizado pelo usuário para enviar para as requisições subsequentes (6).

Da mesma forma, o ambiente ativo segue o mesmo fluxo do ambiente passivo, apenas evitando o handshake necessário no ambiente passivo, que é necessário para descobrir qual é o identity provider responsável pela autenticação. Assim como comentado acima, todas as informações necessárias já foram fornecidas durante a referência do serviço, e as requisições (5) já enviarão o token embutido em seus headers.

Observação: Acima vimos os ambientes passivo e ativo sem mencionar os detalhes de implementação de cada um deles. Isso será alvo de futuros artigos, que irão detalhar cada um dos ambientes, mostrando como proceder para configurar cada um deles.

Os Padrões

Uma das principais características que este modelo autenticação e autorização deve ter, é a interoperabilidade entre as partes envolvidas. Como a ideia permitir uma espécie de layer na internet para gerenciar toda a segurança, inclusive entre domínios, uma das principais exigências é a garantia de que toda e qualquer plataforma pudesse tirar proveito disso.

Atualmente já há várias especificações que regem grande parte das comunicações distribuídas entre plataformas, e que são conhecidas como padrões WS-*. Os padrões que compõem as especificações WS-* já estão há bastante tempo no mercado, e são gerenciados por órgãos independentes. Há padrões para grande parte das necessidades que temos hoje em dia nos sistemas distribuídos, tais como: transações, mensagens confiáveis e segurança. Como esses padrões já estão quase todos finalizados, então porque não utilizá-los? É justamente isso que ocorreu, ou seja, grande parte de toda a comunicação que é efetuada entre as partes deste modelo de autenticação, são realizadas seguindo esses padrões, mas felizmente são transparentes para o usuário final, e abstraídas através dos produtos criados pela Microsoft. Abaixo temos esses padrões elencados, com uma breve descrição de cada um deles:

  • WSDL: Descreve quais funcionalidades (operações) são expostas pelo serviço.
  • WS-Policy: O WSDL não descreve nada sobre os requerimentos de segurança necessários para invocar o serviço. O WS-Policy aborda este caso, fornecendo uma forma genérica de descrever os requerimentos de segurança relacionados ao respectivo serviço.
  • WS-Security: Basicamente, este padrão especifica como aplicar a criptografia nas mensagens SOAP, fazendo com que as informações trafeguem de forma protegida, garantindo a confidencialidade e integridade. A forma de segurança aplicada aqui está condicionada ao formato de autenticação que você utilizará entre o subject e o identity provider. Como a interoperabilidade é um dos aspectos mais importantes, o WS-Security efetua a criptografia das mensagens através de token profiles, que descreve como mapear as tecnologias de autenticação que temos atualmente (KerberosUserNames, certificados, etc.), para um modelo genérico.
  • WS-SecurityPolicy: Fornece um padrão para representar as capacidades e requerimentos dos serviços através de políticas.
  • WS-Trust: Este padrão fornece extensões para o padrão WS-Security, definindo operações específicas de emissão, renovação e validação de tokens.
  • WS-Federation: Organiza em uma linguagem de mais alto nível os padrões WS-Trust e WS-Security, definindo mecanismos para permitir que a autenticação e autorização sejam feitas entre domínios.
  • WS-MetadataExchange: Permite uma forma de extrair, não somente os dados que descrevem as operações do serviço, mas também todas as funcionalidades (de infraestrutura) expostas por aquele serviço.

Além dos padrões acima, ainda temos o SAML (Security Assertion Markup Language). Enquanto o padrão WS-Security define como inserir as informações dentro do envelope SOAP, o SAML ajuda a definir o que essas informações são. O SAML é uma especificação, também baseada em XML, que permite alguém emitir afirmações a respeito de outro alguém ou para alguma coisa, independentemente da identidade que está sendo utilizada. Entre essas afirmações que fazemos sobre algo, elas também podem descrever os atributos deste alguém, que são também conhecidos como claims.

Como disse acima, o padrão WS-Trust fornece um contrato com quatro operações: Issue, Validate, Renew e Cancel. Cada uma dessas operações autoexplicativas, manipulam tokens que são gerados por um STS para seus subjects. Em todos os ambientes que vimos acima (passivo e ativo), em algum momento há um diálogo entre o subject e o STS do identity provider. Nestes casos, o diálogo é realizado para a emissão (Issue) de um token, que depois de emitido será utilizado para enviar para a aplicação (relying party) que está requerendo. Cada uma das operações expostas troca mensagens conhecidas como RST (Request Security Token) e RSTR (Request Security Token Response), especificadas pelo WS-Trust, que entre várias informações, incluem o tipo de token a ser emitido (que nada mais é que a versão do SAML que está sendo utilizada) e as claims que estão sendo solicitadas pela relying party.

Conclusão: No decorrer neste artigo, analisamos os ambientes possíveis que temos quando trabalhamos com este modelo de autenticação, incluindo o cenário federado. Além disso, analisamos como o fluxo das mensagens acontece nestes ambientes, detalhando em alto nível, os passos necessários para atingir o objetivo. Em futuros artigos, vamos explorar detalhadamente como implementar esse modelo em cada um cenários, analisando como configurar e manter um sistema que faz uso destes elementos.

Tags: , ,

CSD | Security | WIF

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