Desenvolvimento - C#

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

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.

por Israel Aéce



function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i 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;
            }
        }
    }
}
Imports System
Imports System.Security.Principal

Namespace XmlSecurity
    Public Class XmlIdentity
        Implements IIdentity

        Private _authenticationType As String
        Private _isAuthenticated As Boolean
        Private _name As String

        Public Sub New(ByVal authenticationType As String, ByVal name As String)
            Me._authenticationType = authenticationType
            Me._name = name
            Me._isAuthenticated = Not (name = String.Empty)
        End Sub

        Public ReadOnly Property AuthenticationType() As String _
            Implements IIdentity.AuthenticationType
            Get
                Return Me._authenticationType
            End Get
        End Property

        Public ReadOnly Property IsAuthenticated() As Boolean _
            Implements IIdentity.IsAuthenticated
            Get
                Return Me._isAuthenticated
            End Get
        End Property

        Public ReadOnly Property Name() As String _
            Implements IIdentity.Name
            Get
                Return Me._name
            End Get
        End Property
    End Class
End Namespace
C# VB.NET

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;
        }
    }
}
Imports System
Imports System.Linq
Imports System.Security.Principal

Namespace XmlSecurity
    Public Class XmlPrincipal
        Implements IPrincipal

        Private _roles As String()
        Private _identity As XmlIdentity

        Public Sub New(ByVal identity As XmlIdentity, ByVal roles As String())
            Me._identity = identity
            Me._roles = roles
        End Sub

        Public ReadOnly Property Identity() As IIdentity _
            Implements IPrincipal.Identity
            Get
                Return Me._identity
            End Get
        End Property

        Public ReadOnly Property Roles() As String()
            Get
                Return Me._roles
            End Get
        End Property

        Public Function IsInRole(ByVal role As String) As Boolean _
            Implements IPrincipal.IsInRole

            Return (From r In Me.Roles Where r = role).Count()> 0
        End Function
    End Class
End Namespace
C# VB.NET

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.");
        }
    }
}
Imports System
Imports System.IdentityModel.Selectors
Imports System.IdentityModel.Tokens

Namespace XmlSecurity
    Public Class XmlAuthentication
        Inherits UserNamePasswordValidator

        Public Overrides Sub Validate(ByVal userName As String, ByVal password As String)
            If Not XmlSecurityHelper.ValidateUser(userName, password) Then
                Throw New SecurityTokenValidationException("Usuario Invalido.")
            End If
        End Sub
    End Class
End Namespace
C# VB.NET

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
    }
}
Imports System
Imports System.Collections.Generic
Imports System.IdentityModel.Claims
Imports System.IdentityModel.Policy
Imports System.Security.Principal

Namespace XmlSecurity
    Public Class XmlAuthorizationPolicy
        Implements IAuthorizationPolicy

        Public Function Evaluate(ByVal evaluationContext As EvaluationContext, _
            ByRef state As Object) As Boolean Implements IAuthorizationPolicy.Evaluate

            Dim identity As IIdentity = GetIdentityFromClient(evaluationContext)
            Dim xmlIdentity As New XmlIdentity(identity.AuthenticationType, identity.Name)

            evaluationContext.Properties("Principal") = _
                New XmlPrincipal(xmlIdentity, XmlSecurityHelper.GetRolesByUserName(xmlIdentity.Name))

            Return True
        End Function

        Private Shared Function GetIdentityFromClient(ByVal evaluationContext As EvaluationContext)
            Dim identity As IIdentity = Nothing
            Dim propertyIdentities As Object = Nothing

            If Not evaluationContext.Properties.TryGetValue("Identities", propertyIdentities) Then
                Throw New Exception("Nenhuma identidade foi encontrada.")
            End If

            Dim identities As IList(Of IIdentity) = TryCast(propertyIdentities, IList(Of IIdentity))
            If Not IsNothing(identities) AndAlso identities.Count> 0 Then identity = identities(0)

            Return identity
        End Function

        "Outros membros
    End Class
End Namespace
C# VB.NET

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";
        }
    }
}
Imports System
Imports System.Security.Permissions

Public Class Servico
    Implements IContrato

    <PrincipalPermission(SecurityAction.Demand, Role:="Administrator")> _
    Public Function RecuperarDados() As String _
        Implements IContrato.RecuperarDados

        "Executará somente se o usuário possuir o papel "Administrator"

        Return "Resultado"
    End Function
End Class
C# VB.NET

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";
        }
    }
}
Imports System
Imports System.Threading
Imports Host.XmlSecurity

Public Class Servico
    Implements IContrato

    Public Function RecuperarDados() As String _
        Implements IContrato.RecuperarDados

        Dim xmlPrincipal As XmlPrincipal = DirectCast(Thread.CurrentPrincipal, XmlPrincipal)

        If xmlPrincipal.IsInRole("Administrator") Then
            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"))
        End If

        Return "Resultado"
    End Function
End Class
C# VB.NET

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
    }
}
Imports System
Imports System.Security.Principal
Imports System.ServiceModel

Namespace XmlSecurity
    Public Class XmlAuthorizationManager
        Inherits ServiceAuthorizationManager

        Protected Overrides Function CheckAccessCore(ByVal operationContext _
                                                     As OperationContext) As Boolean
            MyBase.CheckAccessCore(operationContext)
            Dim xmlPrincipal As XmlPrincipal = GetCurrentXmlPrincipal(operationContext)

            If operationContext.IncomingMessageHeaders.Action = _
                "http://www.projetando.net/IContrato/RecuperarDados" Then
                If Not xmlPrincipal.IsInRole("Administrator") Then
                    Return False
                End If
            End If

            Return True
        End Function

        "O método GetCurrentXmlPrincipal foi omitido
    End Class
End Namespace
C# VB.NET

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());
}
Imports System
Imports Client.Servico

Using proxy As New ContratoClient()
    proxy.ClientCredentials.UserName.UserName = "IsraelAece"
    proxy.ClientCredentials.UserName.Password = "123"

    Console.WriteLine(proxy.RecuperarDados())
End Using
C# VB.NET

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.
Israel Aéce

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