CAS != Windows Security

Uma das funcionalidades mais interessantes do .NET Framework ao meu ver é o CAS – Code Access Security. Toda aplicação que roda sobre a plataforma .NET pode interagir de forma direta ou indireta com o CAS e, com isso demandar os privilégios necessários para que ela possa ser executada.

Quando a aplicação é inicializada, a mesma é carregada para dentro de um processo, que é chamado também de host. Esse host extrai e examina a evidência do Assembly, podendo diferentes informações serem coletadas de acordo com a origem do mesmo. A evidência consiste no strong-name, zona e publicador do Assembly. Depois de capturada, essa evidência é passada para o sistema de segurança do .NET Framework (CAS) para que o mesmo avalie e conceda as devidas permissões para o Assembly.
 
A partir deste momento, baseando-se na evidência extraída do Assembly, o runtime security policy começará a avaliar a “árvore” de code groups. Se a condição for atendida, dizemos que o Assembly é membro do code group e, além disso, o conjunto de permissões (permission set) vinculadas a essa condição será concedido ao Assembly. Caso contrário, possíveis code groups que estiverem abaixo da condição não atendida, não serão avaliados e, obviamente, não serão concedidos. Depois deste processo, a união dos conjuntos de permissões é computada e esse processo é repetido para cada nível das políticas de segurança (policy levels) que são: User, Machine e Enterprise. Finalmente, depois de todos os níveis avaliados, o Assembly receberá a interseção de todos os grupos de permissões entre os níveis, e o Assembly receberá o que chamamos de Final Permission Grant (FG).

Essas permissões são definidas a partir do utilitário caspol.exe para aplicações Windows ou, para aplicações ASP.NET, através dos arquivos *.config que estão localizados no diretório do .NET Framework. Independentemente do tipo de aplicação, mesmo que ela ganhe permissão para acesso ao sistema de arquivos, isso não quer dizer que a aplicação irá executar uma operação (de escrita ou leitura) com sucesso.

A questão é que o CAS garante as permissões que o código tem sobre o sistema operacional. Quando executamos uma aplicação ela está sempre associada à uma identidade dentro do sistema operacional. Em aplicações Windows o padrão é rodar com as credenciais do usuário corrente que está logado no sistema operacional; já em aplicações ASP.NET, o que prevalece é o usuário do IIS, mais precisamente, a identidade definida na criação do worker process. Com esse comportamento, de nada adianta a aplicação receber o FileIOPermission se o usuário que está associado à execução naquele momento não possuir os devidos privilégios de acesso dentro do Windows.

requirePermission

As seções connectionStrings e appSettings são as únicas que podemos acessar diretamente no código sem nenhum tipo de problema, mesmo quando a aplicação está sendo executada em um ambiente parcialmente confiável. Por parcialmente confiável em uma uma aplicação ASP.NET, entenda como o trust level esteja definido como “Medium” ou algo abaixo disso:

<trust level=”Medium”/>

Neste caso, qualquer outra seção (authentication, smtp, globalization, etc.) que tentarmos acessar em um neste ambiente, uma exceção do tipo SecurityException será lançada informando que não possuímos a permissão necessária que, neste caso, é ConfigurationPermission. Note que só o fato de acessar a seção já provoca o disparo da exceção:

AuthenticationSection section =
    (AuthenticationSection)WebConfigurationManager.GetSection(“system.web/authentication”);
Response.Write(section.Mode.ToString());

Quem determina essa restrição é o atributo requirePermission que é configurado no registro da seção dentro do arquivo de configuração. Por padrão, ele é definido como True e, como dito acima, somente as seções connectionStrings e appSettings são definidas como False, como se pode notar no arquivo machine.config. Somente altere a configuração padrão caso voce realmente saiba o que está fazendo e, principalmente, conhecendo as implicações que isso pode causar. Além disso, é importante dizer que voce também pode determinar essa restrição quando voce cria uma seção de configuração customizada.

Acesso anônimo à arquivos *.svc

Ao efetuar o deploy de uma aplicação que utiliza a configuração padrão de um projeto do tipo WCF Service (arquivos *.svc), obrigatoriamente voce precisa permitir o acesso anônimo ao diretório virtual. Caso essa configuração não seja realizada, ao tentar acessar o serviço uma exceção do tipo NotSupportedException será disparada:

Security settings for this service require ‘Anonymous’ Authentication but it is not enabled for the IIS application that hosts this service.

Habilitar a autenticação anônima resolve o problema, mas em alguns cenários isso não é aceitável. A forma de resolver isso é alinhar as configurações de autenticação do serviço (arquivo Web.config) com as configurações do diretório virtual. O primeiro passo é desabilitar o acesso anônimo no diretório virtual, permitindo apenas a autenticação Windows. Neste caso,  a configuração do binding deve ser definida como autenticação Windows, refletindo a mesma configuração do IIS; já quando o modo de seguraça estiver definida como TransportCredentialOnly que “desliga” o acesso anônimo ao serviço. Com a configuração abaixo já é possível acessar o serviço sem o acesso anônimo habilitado no IIS:

<bindings>
  <basicHttpBinding>
    <binding name=”bndConfig”>
      <security mode=”TransportCredentialOnly”>
        <transport clientCredentialType=”Windows”/>
      </security>
    </binding>
  </basicHttpBinding>
</bindings>

Um outro detalhe importante, é que endpoints para publicação do documento WSDL também exigem a autenticação anônima, e se tiver este endpoint no serviço, a mesma exceção que vimos acima será disparada. Para contornar este problema, voce deverá utilizar as mesmas regras de configurações de autenticação para este endpoint mas, os bindings exclusivos para publicação de metadados (mex*) não possuem características de segurança que possam ser configuradas. Podemos configurar o endpoint que expõe os metadados a partir de bindings convencionais e, com isso, ter acesso à todas as configurações de segurança que o WCF fornece. Abaixo consta a configuração geral deste cenário:

<endpoint
    address=””
    binding=”basicHttpBinding”
    contract=”IService”
    bindingConfiguration=”bndConfig” />
<endpoint
    address=”mex”
    binding=”basicHttpBinding”
    contract=”IMetadataExchange”
    bindingConfiguration=”bndConfig” />

<bindings>
  <basicHttpBinding>
    <binding name=”bndConfig”>
      <security mode=”TransportCredentialOnly”>
        <transport clientCredentialType=”Windows”/>
      </security>
    </binding>
  </basicHttpBinding>
</bindings>

.NET 3.5 SP1 GDR (Fixes)

Está disponível a partir deste link uma atualização para o .NET Framework 3.5 SP1. Esta atualização também faz algumas poucas mudanças no WCF e, entre elas, o envio correto do código de status do protocolo HTTP quando a autenticação via HTTP é inválida.

Como já foi mencionado neste kb, quando utilizamos um autenticador customizado no WCF, disparamos a exceção SecurityTokenValidationException para informar ao runtime que o usuário é inválido. Quando o runtime identifica essa exceção, ele traduz a mesma para o código 403 (Forbidden) do HTTP. A mudança que é realizada com esta atualização é que ao invés de retornar 403, passará a retornar o código 401 (Unauthorized), que acaba sendo mais coerente com a situação.

WCF – Segurança – Autenticação e Autorização Customizadas

Como já foi detalhado neste artigo, o WCF fornece várias possibilidades de gerenciar a autenticação e autorização dentro dos serviços. Uma dessas possibilidades é customizar como o WCF deverá autenticar e autorizar o cliente, analisando as suas credenciais, verificando se essas são válidas em um determinado repositório, determinar quais são os direitos que o cliente tem no serviço e, finalmente, conceder ou negar o acesso a alguma operação baseando-se em seus privilégios. A finalidade deste artigo é analisar os passos necessários para essa customização.

Uma das grandes necessidades que se tem atualmente é permitir ao cliente fornecer um usuário e senha e, do lado do serviço, verificar se ele é válido ou não em algum repositório, como um banco de dados. O mais próximo disso que existe dentro do WCF é a integração com o MembershipProvider para autenticação e RoleProvider para autorização, fornecidos pelo ASP.NET 2.0. Podemos recorrer a estas APIs, que seguem o padrão Provider Model (System.Web), para a criação customizada de um provider que atenda a nossa necessidade e, depois disso, acoplá-las no WCF. Para obter um maior controle, estas APIs não serão utilizadas neste exemplo.

Como a idéia é mostrar como efetuar a autenticação e autorização de forma customizada, o foco do artigo será validar o usuário e recuperar seus respectivos papéis de arquivos XML. É importante dizer que isso apenas servirá como exemplo para o artigo e não deve ser utilizado em um ambiente real, devido aos problemas de performance e, principalmente, de segurança. Dois arquivos, padrão XML, serão utilizados como “base de dados”, sendo um para o armazenamento dos usuários e outro para os papéis destes usuários. Abaixo é exibida a estrutura destes dois arquivos:

<?xml version="1.0" encoding="utf-8" ?>
<users>
  <user name="IsraelAece" password="123" />
  <user name="JulianoAece" password="456" />
</users>
UsersRepository.xml  

 

<?xml version="1.0" encoding="utf-8" ?>
<rolesRepository>
  <user name="IsraelAece">
    <role name="Administrator" />
    <role name="IT" />
  </user>
  <user name="JulianoAece">
    <role name="IT" />
  </user>
</rolesRepository>
RolesRepository.xml  

Como podemos notar, o primeiro arquivo serve de repositório para todos os usuários cadastrados no sistema, armazenando o seu nome (que servirá como login) e a senha de acesso. Já o segundo arquivo, armazena os papéis que um determinado usuário tem no sistema, e a relação se dá pelo próprio nome do usuário, através do atributo name do elemento user.

Antes de falar sobre as peculiaridades do WCF, precisamos entender alguns conceitos de segurança que existem dentro da plataforma .NET desde a versão 1.0. Duas Interfaces são utilizadas como base para os mecanismos de autenticação e autorização: IIdentity e IPrincipal (namespace System.Security.Principal), respectivamente. A Interface IIdentity fornece três propriedades autoexplicativas: Name, AuthenticationType e IsAuthenticated. Já a segunda possui dois membros que merecem uma atenção especial. O primeiro deles é a propriedade Identity que retorna a instância de uma classe que implemente a Interface IIdentity, representando a identidade do usuário; já o segundo membro trata-se de um método chamado IsInRole que, dado uma papel, retorna um valor boleano indicando se o usuário corrente possui aquele papel. Como podemos notar, as classes de autenticação e autorização trabalham em conjunto.

Dentro do namespace System.Threading existe uma classe chamada Thread. Essa classe determina como controlar uma thread dentro da aplicação. Essa classe, entre vários membros, possui uma propriedade estática chamada CurrentPrincipal que recebe e retorna uma instância de um objeto que implementa a Interface IPrincipal. É através desta propriedade que devemos definir qual será a identity e principal que irá representar o contexto de segurança para a thread atual.

Há algumas implementações das Interfaces IIdentity e IPrincipal dentro do .NET Framework, como é o caso das classes GenericIdentity, WindowsIdentity, GenericPrincipal e WindowsPrincipal. Apesar das classes GenericIdentity e GenericPrincipal servirem para o exemplo, vamos criar a nossa própria implementação, que neste caso chamará: XmlIdentity e XmlPrincipal.

As propriedades expostas pela Interface IIdentity são de somente-leitura, o que nos obriga a passar as informações como o tipo de autenticação e o nome do usuário através de um construtor. Justamente por isso a classe XmlIdentity deve fornecer um construtor com, no mínimo, estes dois parâmetros, podendo inclusive criar diferentes versões dele para suportar as propriedades que você julgar necessário, já que desta forma temos controle total. O código abaixo mostra na íntegra a implementação desta classe que será utilizada por todo o exemplo:

using System;
using System.Security.Principal;

namespace Host.XmlSecurity
{
    internal class XmlIdentity : IIdentity
    {
        private string _authenticationType;
        private bool _isAuthenticated;
        private string _name;

        public XmlIdentity(string authenticationType, string name)
        {
            this._authenticationType = authenticationType;
            this._name = name;
            this._isAuthenticated = (name != string.Empty);
        }

        public string AuthenticationType
        {
            get
            {
                return this._authenticationType;
            }
        }

        public bool IsAuthenticated
        {
            get
            {
                return this._isAuthenticated;
            }
        }

        public string Name
        {
            get
            {
                return this._name;
            }
        }
    }
}

O próximo passo é criar a classe responsável por armazenar as informações necessárias para efetuar a autorização. Como falado anteriormente, esse tipo de classe deve implementar a Interface IPrincipal e, neste caso, chamaremos de XmlPrincipal. Essa classe também deve fornecer um construtor que permita informarmos a identidade (classe que implemente a Interface IIdentity) e os papéis que aquele usuário possuir, e ambas informações serão armazenadas em campos privados desta mesma classe. Uma propriedade chamada Roles foi criada apenas por conveniência, expondo os papéis daquele usuário.

Por fim, o método IsInRole tem papel extremamente importante. Ao utilizar o modo declarativo ou imperativo para verificar se o usuário possui um papel específico, indiretamente o WCF irá interrogar este método, que deverá retornar um valor boleano indicando se o usuário possui ou não aquele papel. Basicamente ele deverá percorrer o array de strings (que são os papéis) e verificar se o papel que é passado como parâmetro está contido neste array. Abaixo está a classe XmlPrincipal, e podemos notar que em seu construtor, além dos papéis, ela também recebe a identidade do usuário, que está tipificada como XmlIdentity.

using System;
using System.Linq;
using System.Security.Principal;

namespace Host.XmlSecurity
{
    internal class XmlPrincipal : IPrincipal
    {
        private string[] _roles;
        private XmlIdentity _identity;

        public XmlPrincipal(XmlIdentity identity, string[] roles)
        {
            this._identity = identity;
            this._roles = roles;
        }

        public IIdentity Identity
        {
            get
            {
                return this._identity;
            }
        }

        public string[] Roles
        {
            get
            {
                return this._roles;
            }
        }

        public bool IsInRole(string role)
        {
            return (from r in this.Roles where r == role select r).Count() > 0;
        }
    }
}

O que vimos nos códigos acima não é exclusividade do WCF. A partir de agora vamos começar a analisar as classes que podem ser utilizadas para a customização da autenticação e autorização dentro do WCF. O primeiro detalhe importante é que para utilizar alguns tipos, devemos referenciar dois assemblies na aplicação que corresponde ao serviço: System.IdentityModel.dll e System.IdentityModel.Selectors.dll. Esses assemblies possuem vários tipos utilizados para gerir os processos de autenticação, autorização, tokes, claims, a customização de tudo isso, entre várias outras utilidades.

Como a autenticação sempre ocorre antes da autorização, vamos iniciar por ela, analisando a classe que permitirá tal customização. Para customizar a validação do login e senha informados pelo usuário, temos uma classe abstrata chamada UserNamePasswordValidator (namespace System.IdentityModel.Selectors) que especifica como será efetuada essa validação, sobrescrevendo o método Validate que, por sua vez, recebe o login e senha como parâmetro e retorna uma valor boleano. Como o exemplo irá extrair essas informações de um arquivo XML (UsersRepository.xml), é dentro deste que devemos efetuar a busca. A implementação desta classe é exibida abaixo:

using System;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;

namespace Host.XmlSecurity
{
    internal class XmlAuthentication : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            if (!XmlSecurityHelper.ValidateUser(userName, password))
                throw new SecurityTokenValidationException("Usuario Invalido.");
        }
    }
}

Como podemos ver, o login e senha são encaminhados para o método estático ValidateUser da classe XmlSecurityHelper, que efetua a validação e retorna True caso um usuário com este login e senha seja encontrado dentro dele. É importante dizer que a classe XmlSecurityHelper não faz parte do .NET Framework. Ela foi customizada e utiliza o LINQ To XML para encontrar as informações dentro do arquivo. Para poupar espaço, ela não será exibida aqui mas, ao efetuar o download do exemplo, você poderá explorá-la. Caso você utilize algum outro repositório como o SQL Server, você pode efetuar neste mesmo local uma query para determinar a existência do usuário. Caso nenhum usuário seja encontrado, uma exceção do tipo SecurityTokenValidationException é disparada, evitando que o cliente acesse o serviço.

É importante dizer que o usuário somente estará autenticado depois do retorno deste método, pois o WCF fará as manipulações necessárias para que isso aconteça. Se analisar a propriedade estática CurrentPrincipal da classe Thread, verá que a identidade do usuário recém validado ainda não estará lá. Para evitar maiores problemas, não se deve confiar nesta propriedade antes deste método retornar. Tudo o que veremos a partir de agora somente estará acessível ao usuário caso ele tenha sido devidamente autenticado.

Depois da classe que valida a existência do usuário, chega o momento de customizar a autorização do mesmo. Essa customização consiste em dois passos: a criação de uma política de autorização e, opcionalmente, a criação de um gerenciador de autorização. O segundo passo somente se faz necessário quando desejamos centralizar a validação em um único lugar, evitando poluir a classe que representa o serviço com informações relacionadas a segurança. De qualquer forma, falaremos mais detalhadamente sobre este segundo passo mais adiante.

O primeiro passo é a construção de uma política de autorização de usuários. Para criar esta política é necessário implementar a Interface IAuthorizationPolicy (namespace System.IdentityModel.Policy). A classe que implementa esta Interface não tem a finalidade de autorizar o usuário, mas será responsável por criar a classe principal referente a ele, extrair os seus papéis e devolver a instância da classe principal para o WCF, que fará uso dela posteriormente para determinar se ele tem ou não acesso a um determinado recurso/operação.

Ao implementar a Interface IAuthorizationPolicy em uma classe, você será obrigado a customizar os três membros fornecidos por ela. O primeiro deles, a propriedade Id, retorna uma string que identifica o componente; já a propriedade Issuer retorna uma das opções fornecidas pelo enumerador ClaimSet, indicando quem é o emissor daquela política. Finalmente, o último e mais importante membro desta Interface, é o método Evaluate. Esse método será executado em todas as requisições, e a finalidade dele, é avaliar se o usuário se enquadra nos requerimentos desta política e, além disso, podemos utilizar este método para definir o contexto de segurança do usuário atual, através das classes identity e principal.

Como parâmetro, este método recebe uma instância da classe EvaluationContext que representa os resultados das políticas de autorização que foram avaliados. O motivo do método Evaluate retornar uma valor boleano é porque o WCF permite adicionar várias políticas de autorização, e o retorno deste método irá determinar se a política seguinte deverá ou não ser analisada. Este método também recebe como parâmetro um object, e se dentro deste método você mudar o valor dele (não nulo), esta informação será encaminhada para as políticas subsequentes. Abaixo é exibida parcialmente a classe que implementa a Interface IAuthorizationPolicy, focando apenas no método Evaluate:

using System;
using System.Collections.Generic;
using System.IdentityModel.Claims;
using System.IdentityModel.Policy;
using System.Security.Principal;

namespace Host.XmlSecurity
{
    internal class XmlAuthorizationPolicy : IAuthorizationPolicy
    {
        public bool Evaluate(EvaluationContext evaluationContext, ref object state)
        {
            IIdentity identity = GetIdentityFromClient(evaluationContext);
            XmlIdentity xmlIdentity = 
new XmlIdentity(identity.AuthenticationType, identity.Name); evaluationContext.Properties["Principal"] = new XmlPrincipal( xmlIdentity, XmlSecurityHelper.GetRolesByUserName(xmlIdentity.Name)); return true; } private static IIdentity GetIdentityFromClient(EvaluationContext evaluationContext) { IIdentity identity = null; object propertyIdentities = null; if (!evaluationContext.Properties.TryGetValue("Identities",
out propertyIdentities)) throw new Exception("Nenhuma identidade foi encontrada."); IList<IIdentity> identities = propertyIdentities as IList<IIdentity>; if (identities != null && identities.Count > 0) identity = identities[0]; return identity; } //Outros membros } }

A primeira tarefa a ser executada dentro do método Evaluate é extrair as credenciais do usuário, pois não há como saber os papéis sem antes encontrar quem é o usuário. Para mais legibilidade, um método privado e estático chamado GetIdentityFromClient foi criado para isso, retornando a identidade do usuário corrente. Este método recorre à propriedade Properties da classe EvaluationContext, que retorna um dicionário contendo a coleção de informações que não estão relacionadas aos claims e “Identities” é uma delas.

Neste cenário, este método sempre retornará uma instância da classe GenericIdentity mas, como criamos a nossa própria versão de identidade (XmlIdentity), devemos fazer uso dela neste momento, instanciando-a e passando para o seu construtor as informações que estão a identidade corrente do usuário. A propriedade AuthenticationType retornará uma string contendo o tipo que efetuou a validação do usuário, que no nosso caso foi o “XmlAuthentication”, enquanto a propriedade Name retorna o nome do usuário autenticado.

Depois da nova identidade criada devemos criar a principal, representada pelo tipo XmlPrincipal que vimos mais acima. O único construtor fornecido por esta classe possui dois parâmetros: a identidade (classe que implemente a Interface IIdentity) e uma string contendo os papéis do usuário. A identidade já foi criada e está armazenada na variável xmlIdentity e os papéis serão extraídos do arquivo XML (RolesRepository.xml), através do método estático GetRolesByUserName da classe XmlSecurityHelper. A instância da classe XmlPrincipal será acomodada no mesmo dicionário de onde extraímos a identidade do usuário, ou seja, na propriedade Properties da classe EvaluationContext, sob a chave “Principal”.

Como falado anteriormente, as classes que implementam a Interface IAuthorizationPolicy não efetua a autorização em si, que consiste em verificar se o usuário tem ou não permissão para acessar um determinado recurso ou operação. Para proteger um recurso ou uma operação como um todo, podemos recorrer a forma declarativa ou imperativa de efetuar a verificação. No modo declarativo, decoramos a operação do serviço com o atributo PrincipalPermissionAttribute (namespace System.Security.Permissions), que através da propriedade Role podemos informar o papel que o usuário deverá possuir para acessá-la. O exemplo de código abaixo ilustra esta técnica:

using System;
using System.Security.Permissions;

namespace Host
{
    public class Servico : IContrato
    {
        [PrincipalPermission(SecurityAction.Demand, Role = "Administrator")]
        public string RecuperarDados()
        {
            //Executará somente se o usuário possuir o papel "Administrator"

            return "Resultado";
        }
    }
}

Com o modelo acima, caso o usuário não possua o papel “Administrator”, uma exceção do tipo SecurityAccessDeniedException será disparada e o método não será executado. Já para ter um controle mais refinado sobre os papéis e direitos que o usuário terá dentro da operação, podemos recorrer ao modo imperativo e, através do método IsInRole da classe que representa a principal, verificamos se ele possui ou não um determinado papel. A classe XmlAuthorizationPolicy que vimos acima foi responsável por criar a XmlPrincipal e, quando foi devolvido para o WCF, ele se encarregou de armazená-la na propriedade estática CurrentPrincipal da classe Thread. Abaixo temos a mesma operação, só que agora com um maior controle, mas não deixando de se preocupar quando o usuário não possuir os papéis necessários para executar alguma tarefa.

using System;
using System.Threading;
using Host.XmlSecurity;

namespace Host
{
    public class Servico : IContrato
    {
        public string RecuperarDados()
        {
            XmlPrincipal xmlPrincipal = (XmlPrincipal)Thread.CurrentPrincipal;

            if (xmlPrincipal.IsInRole("Administrator"))
            {
                Console.WriteLine("Name: " + xmlPrincipal.Identity.Name);
                Console.WriteLine("Identity Type: " + 
xmlPrincipal.Identity.GetType().FullName); Console.WriteLine("Authentication Type: " + xmlPrincipal.Identity.AuthenticationType); Console.WriteLine("Is In IT Role? " + xmlPrincipal.IsInRole("IT")); } return "Resultado"; } } }

Com esta última técnica, temos um controle maior sobre como conceder ou negar acesso a um determinado recurso através dos papéis do usuário. Mas os grandes problemas que existem em ambas as técnicas é a “poluição” da classe que representa o serviço (regras de negócios) com códigos exclusivos de segurança e uma possível duplicação de código. Visando facilitar isso, o WCF disponibiliza uma classe chamada ServiceAuthorizationManager (namespace System.ServiceModel).

Essa classe fornece métodos para a verificação de autorização das operações do serviço, sendo invocados em todas as requisições realizadas. Além disso, ela é responsável por carregar todas as políticas de autorização existentes (classes que implementam a Interface IAuthorizationPolicy), invocando o método Evaluate de cada uma delas. Uma vez que todas as políticas de autorização forem avaliadas, a classe ServiceAuthorizationManager terá acesso ao conjunto final de papéis e, a partir daí, tomar decisões baseando-se neles.

Para customizar, podemos criar uma classe de gerenciamento de autorização, obviamente herdando da classe ServiceAuthorizationManager. Essa classe fornece dois métodos virtuais chamados CheckAccess e CheckAccessCore, que retornam um valor boleano indicando se o usuário corrente tem ou não permissão de acesso. Escolher entre um deles dependerá se você precisa ou não de dados que estão no corpo da mensagem para tomar a decisão de autorização de acesso. O método CheckAccess possui um overload que, além de fornecer o contexto da operação corrente, disponibiliza um segundo parâmetro do tipo Message, que representa a mensagem atual. Caso um desses métodos, quando sobrescrito, retornar False por algum motivo, uma exceção do tipo SecurityAccessDeniedException será disparada, informando o cliente que ele não possui direitos de acesso.

Como nosso exemplo não precisa analisar nenhum conteúdo da mensagem, então sobrescreveremos diretamente o método CheckAccessCore. Este método recebe como parâmetro o contexto atual, representado pela classe OperationContext e, é através dela que iremos extrair a instância da classe XmlPrincipal, criada por nossa política de autorização (XmlAuthorizationPolicy). Na sequência utilizaremos a coleção de headers para determinar qual operação está sendo invocada e, para isso, recorremos à propriedade Action. A nossa regra consistirá em verificar se a operação requerida é a RecuperarDados e, caso seja, somente se o usuário atual possui o papel Administrator poderá acessá-la, conforme é mostrado abaixo:

using System;
using System.Security.Principal;
using System.ServiceModel;

namespace Host.XmlSecurity
{
    internal class XmlAuthorizationManager : ServiceAuthorizationManager
    {
        protected override bool CheckAccessCore(OperationContext operationContext)
        {
            base.CheckAccessCore(operationContext);
            XmlPrincipal xmlPrincipal = GetCurrentXmlPrincipal(operationContext);

            if (operationContext.IncomingMessageHeaders.Action == 
                "http://www.projetando.net/IContrato/RecuperarDados")
                if (!xmlPrincipal.IsInRole("Administrator"))
                    return false;

            return true;
        }

        //O método GetCurrentXmlPrincipal foi omitido
    }
}

Depois de todas essas implementações que foram feitas, as classes por si só não funcionam. Elas precisam ser acopladas na execução do serviço, mas precisamente no host (ServiceHost) que hospeda o serviço para que o host em conjunto com o runtime do WCF faça uso delas. Para efetuar a configuração delas, podemos optar pelo modelo declarativo ou imperativo mas, por questões de espaço, ela será realizada utilizando o modelo declarativo, ou seja, através do arquivo App.config ou Web.config.

Para utilizar a autenticação baseada em UserName/Password que o WCF fornece sob o protocolo HTTP, será necessário utilizar um certificado, pois toda a segurança será garantida pela mensagem (para mais detalhes sobre a segurança de serviços WCF, consulte este artigo). Sem a utilização deste, não seria possível garantir a integridade e confidencialidade da mensagem, comprometendo as informações e, principalmente, permitindo que alguém intercepte a mensagem e capture os dados sigilosos.

O código abaixo ilustra todas as configurações necessárias para fazer com que as classes que implementamos acima funcionem. Para explicar melhor, vamos dividir o arquivo em duas seções: uma para falar das configurações básicas do serviço (bindings, endpoints, etc.) e a segunda para detalhar a configuração da autenticação e autorização.

Como podemos notar, estamos definindo o serviço no arquivo de configuração contendo dois endpoints, sendo um para a publicação dos metadados (WSDL) e o outro para enviar requisições para o serviço. Tanto o serviço como os metadados estão acessíveis através do protocolo HTTP. O endpoint do serviço utiliza o binding wsHttpBinding e, para configurá-lo, define no atributo bindingConfiguration o valor srvBindingConfig que aponta para uma seção um pouco mais abaixo. Nesta seção definimos que o modo de segurança será baseado na mensagem e o tipo da credencial será UserName, obrigando o cliente a fornecer o login e senha antes de executar a operação. É importante notar que o serviço, através do atributo behaviorConfiguration, aponta para uma seção de behaviors chamada srvBehaviorConfig que falaremos a seguir.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Host.Servico" behaviorConfiguration="srvBehaviorConfig">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8778/"/>
          </baseAddresses>
        </host>
        <endpoint
          address="mex"
          binding="mexHttpBinding"
          contract="IMetadataExchange" />
        <endpoint 
          address="srv" 
          binding="wsHttpBinding" 
          contract="Host.IContrato" 
          bindingConfiguration="srvBindingConfig" />
      </service>
    </services>
    <bindings>
      <wsHttpBinding>
        <binding name="srvBindingConfig">
          <security mode ="Message">
            <message clientCredentialType="UserName" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="srvBehaviorConfig">
          <serviceCredentials>
            <serviceCertificate
              findValue="3c f2 d0 c0 ef 8e 0a 96 42 36 e6 54 5f 67 50 e0"
              storeLocation="LocalMachine"
              storeName="My"
              x509FindType="FindBySerialNumber" />
            <userNameAuthentication 
              userNamePasswordValidationMode="Custom" 
              customUserNamePasswordValidatorType="Host.XmlSecurity.XmlAuthentication, Host" />
          </serviceCredentials>
          <serviceAuthorization 
            principalPermissionMode="Custom"
            serviceAuthorizationManagerType="Host.XmlSecurity.XmlAuthorizationManager, Host">
            <authorizationPolicies>
              <add policyType="Host.XmlSecurity.XmlAuthorizationPolicy, Host" />
            </authorizationPolicies>
          </serviceAuthorization>
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>
*.config  

O behavior de serviço chamado srvBehaviorConfig é onde iremos definir toda a configuração de segurança do nosso serviço. No primeiro sub-elemento, chamado serviceCredentials, como o próprio nome diz, devemos configurar as credenciais que serão utilizadas pelo serviço e pelo cliente. Este elemento possui várias configurações para os mais diferentes meios de autenticação. Entre eles temos os elementos serviceCertificate e userNameAuthentication. O primeiro irá definir um certificado para ser utilizado para a proteção da mensagem, como já comentado anteriormente. O segundo elemento, userNameAuthentication fornece duas propriedades: userNamePasswordValidationMode e customUserNamePasswordValidatorType. Definindo a primeira delas como Custom diz ao WCF que vamos customizar a autenticação do usuário utilizando a classe que é definida no atributo customUserNamePasswordValidatorType e, de acordo com nosso exemplo, é responsabilidade da classe XmlAuthentication.

O segundo sub-elemento, serviceAuthorization, é responsável por configurar como será realizada a autorização do usuário. Entre os atributos fornecidos por esse elemento, temos: principalPermissionMode e serviceAuthorizationManagerType. Assim como na configuração anterior, o primeiro atributo determina que a configuração será customizada, enquanto a segunda especificará qual será a classe responsável por gerenciar a autorização (XmlAuthorizationManager). Ainda sobre este elemento, ele possui uma coleção chamada authorizationPolicies, onde podemos adicionar as classes que implementam a Interface IAuthorizationPolicy, já discutida acima.

Observação: Os tipos que são especificados no arquivo de configuração devem possuir o nome completo, incluindo possíveis namespaces e, obrigatoriamente, o nome do assembly onde ele reside.

Configuração do Cliente

Ao efetuar a referência do serviço no cliente, automaticamente algumas configurações já são definidas com os valores corretos, como por exemplo, a configuração do binding, o modo de segurança e a forma de fornecimento das credenciais para o serviço. Lembrando que o serviço expõe um certificado para proteger o envio e/ou recebimento das mensagens de forma segura e, ao efetuar a referência, a chave pública já é fornecida e devidamente configurada.

Depois destas considerações, a única diferença será o fornecimento explícito das credenciais do usuário antes de invocar a operação. Para fornecer as credenciais (login e senha), utilizamos a propriedade ClientCredentials fornecida pelo proxy (ClientBase<TChannel>). Essa propriedade retorna uma instância da classe ClientCredentials que, por sua vez, possui várias propriedades que permitem ao cliente configurar suas credenciais. Entre elas temos a propriedade UserName, do tipo UserNamePasswordClientCredential, que expõe as propriedades UserName e Password. O código abaixo ilustra como acessar essas propriedades, definir os valores e invocar a operação:

using System;
using Client.Servico;

using (ContratoClient proxy = new ContratoClient())
{
    proxy.ClientCredentials.UserName.UserName = "IsraelAece";
    proxy.ClientCredentials.UserName.Password = "123";

    Console.WriteLine(proxy.RecuperarDados());
}

Conclusão: O artigo demonstrou a customização da autenticação e autorização de um serviço WCF baseando-se no modelo de role-based security, mas pode-se adotar as mesmas estratégias para fazer com que o serviço utilize o modelo de identity-based security, apesar de que a Microsoft trabalha em cima de uma nova API para facilitar a construção deste modelo. A finalidade do artigo foi tentar exemplificar detalhadamente como efetuar tal customização que, em um ambiente real, deverá ter alguns cuidados extras que se deverá ter durante o seu desenvolvimento e que não foram abordados no artigo por estar fora do escopo do mesmo.

SegurancaCustomizada.zip (224.56 kb)

UserName e Certificados

Os ASP.NET Web Services fornecem uma possibilidade de efetuar a autenticação do serviço através de SOAP Headers. Para isso, bastava criar uma classe que herde da classe SoapHeader e criar as propriedades UserName/Password e, via SoapHeaderAttribute, voce vinculava este header aos métodos que exigem a autenticação do usuário para poder funcionar.

Essa configuração funciona bem, mas é vulnerável. A questão é que o envelope SOAP irá trafegar entre o cliente e o serviço de forma desprotegida e, sendo assim, qualquer um que interceptar a requisição, conseguirá extrair essas informações confidenciais. A autenticação baseada em SOAP Headers apenas são seguras se o transporte garantir a segurança; em outras palavras, isso quer dizer HTTPS. Outra alternativa ainda utilizando os tradicionais ASP.NET Web Services, seria a utilização do WSE (Web Services Enhancements), que possibilita a segurança em nível de mensagens.

O WCF, que é seguro por padrão, tem um comportamento ligeiramente diferente em relação aos ASP.NET Web Services. Para utilizar a autenticação baseada em UserName/Password que o WCF fornece sob o protocolo HTTP, será necessário utilizar um certificado. Sem a utilização deste, não seria possível garantir a integridade e confidencialidade da mensagem, comprometendo as informações e, principalmente, permitindo que alguém intercepte a mensagem e capture os dados sigilosos. Assim como os ASP.NET Web Services, se o serviço WCF for exposto via HTTPS, isso não é necessário, mas utilizar segurança baseada em transporte tem seu ponto negativo: a segurança é apenas garantida ponto-a-ponto e, caso haja intermediários entre o cliente e o serviço, não temos a garantia de que a mensagem chegará segura até o destino.

encodedValue

Quando fazemos uma referencia a um serviço WCF, e ele possuir um certificado definido para assegurar a troca de informações entre o cliente o e serviço, a IDE do Visual Studio ou o utilitário svcutil.exe, adicionanão um elemento chamado identity/certificate com o atributo encodedValue. O valor deste atributo é uma série de letras e números mas, o que isso representa?

Essa informação trata-se da chave pública do certificado, codificada no padrão Base64. Ela será utilizada pelo runtime do WCF para criptografar as informações que serão trocadas, mais precisamente, as credenciais. Quando essa informação não existir no cliente, a configuração do serviço deverá permitir a negociação ou informar a referencia para o certificado que, geralmente, esta armazenado em um dos repositórios do cliente.

Segurança em SQLCLR

O .NET Framework 2.0 em conjunto com o SQL Server 2005 permite criarmos Stored Procedures, FunctionsTriggers em código gerenciado (C# ou VB.NET). Quando o código que colocamos dentro destes objetos são códigos puramente de acesso à dados, não encontramos problemas no momento de catalogar o Assembly dentro do SQL Server.

Alguns cuidados devem ser tomados quando, dentro destes códigos, há acesso à recursos externos, como IO, Threading, SMTP, etc. Ao catalogar o Assembly, podemos definir uma propriedade chamada PERMISSION_SET, que aceita uma das tres opções a seguir:

  • SAFE: É o padrão. Neste modo o Assembly somente poderá rodar no contexto local, mas não através do SqlClient. Previne também o acesso através de recursos externos e de código não gerenciado.
  • EXTERNAL_ACCESS: É o mesmo que SAFE, somente habilitando o acesso aos recursos externos.
  • UNSAFE: Acesso irrestrito, desde que o Assembly seja assinado e catalogado por um usuário que seja membro do grupo sql_admins.

Quando temos acesso a recurso externo, então teoricamente devemos definir a PERMISSION_SET como EXTERNAL_ACCESS. O problema é que, por padrão, isso não é permitido e se tentarmos, a seguinte mensagem de erro será retornada:

CREATE ASSEMBLY for assembly ‘%’ failed because assembly ‘%’ is not authorized for PERMISSION_SET = EXTERNAL_ACCESS.  The assembly is authorizedwhen either of the following is true: the database owner (DBO) has EXTERNAL ACCESS ASSEMBLY permission and the database has the TRUSTWORTHY database property on; or the assembly is signed with a certificate or an asymmetric key that has a corresponding login with EXTERNAL ACCESS ASSEMBLY permission.

Como a própria mensagem de erro diz, bastaríamos definir a propriedade TRUSTWORTHY do banco de dados como ON, permitindo assim o acesso aos recursos utilizados. O problema é que isso não é recomendado pela Microsoft, já que possibilitaria que qualquer Assembly seja catalogado, até mesmo Assemblies maliciosos.

Para contornar o problema e catalogar o Assembly concedendo a ele, privilégios para acesso à recursos externos, a solução é a criação de de uma chave assimétrica (baseada em chave pública/privada). Para isso, utilizamos a opção CREATE ASYMMETRIC KEY juntamente com a opção FROM EXECUTABLE FILE; isso fará com que a chave pública seja importada do Assembly (devidamente assinado com Strong Name) onde está o código gerenciado com os objetos SQL e irá criá-la dentro do banco de dados master. Depois disso, devemos criar um login e vinculá-lo a esta chave assimétrica que acabamos de gerar. Finalmente damos permissão para acesso à recursos externos à este login, como é mostrado no código abaixo:

CREATE ASYMMETRIC KEY MinhaChave FROM EXECUTABLE FILE = ‘C:SQLExtensibility.dll’
CREATE LOGIN MeuLogin FROM ASYMMETRIC KEY MinhaChave
GRANT EXTERNAL ACCESS ASSEMBLY TO MeuLogin

Depois disso, basta catalogar o Assembly via IDE ou até mesmo via código, concedendo à ele EXTERNAL_ACCESS, como é mostrado no código abaixo:

CREATE ASSEMBLY RecursosExtras
FROM ‘C:SQLExtensibility.dll’
WITH PERMISSION_SET = EXTERNAL_ACCESS

Apesar da primeira forma (TRUSTWORTHY) ser muito mais simples, o melhor é seguir a recomendação da Microsoft, não concedendo TRUSTWORTHY para a base de dados que apenas utilizam recursos externos a partir de um Assembly CLR.

Integrando Windows Live ID ao ASP.NET

Como todos sabem, o Windows Live ID é a evolução do Passport. Antigamente, para integrar uma aplicação ASP.NET ao sistema de autenticação do Passport, além de uma SDK que voce precisa entender e customizar, havia custos envolvidos, o que fez com que a terceira forma de autenticação suportada pelo ASP.NET não vingasse.

Atualmente, o Windows Live ID torna-se muito menos complexo e mais simples de acoplar à aplicações ASP.NET e, além disso, não é mais necessário pagar para utilizá-lo. Tudo o que precisa ser feito é um cadastro prévio, que consiste nas informações a respeito da aplicação que fará o uso do Windows Live ID. Para um passo à passo de como configurá-lo, podemos seguir o um artigo que neste endereço.

Além disso, ainda temos (ainda em CTP) o Windows Live Tools, que adiciona alguns controles na barra de ferramentas do Visual Studio .NET. Este CTP contempla, entre vários controles, os controles IDLoginView, IDLoginStatus e a classe LiveMembershipProvider. Esta última, herda diretamente da classe SqlMembershipProvider e traz a possibilidade de integrar uma credencial Live a um usuário dentro da estrutura de segurança do ASP.NET.