Desenvolvimento - C#

WCF - Segurança

A finalidade deste artigo é exibir todas as possibilidades que temos para manipular a segurança em serviços WCF.

por Israel Aéce



function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i Baixe o código.

Um dos grandes desafios de um software é a segurança do mesmo. Em qualquer software hoje em dia a segurança não consiste apenas em autenticar um usuário, mas também que direitos ele tem dentro do software. As coisas ficam mais complicadas quando falamos de um ambiente distribuído, tornando o processo de autenticação e autorização um pouco mais complexo e, como se não bastasse, temos que nos preocupar com a proteção das requisições que viajam entre o cliente e o servidor.

A finalidade deste artigo é exibir todas as possibilidades que temos para manipular a segurança em serviços WCF. Algumas técnicas serão comentadas mas não serão esgotadas, pois elas merecem um artigo exclusivo para o seu completo entendimento. Já outras técnicas (as mais comuns) de autenticação, autorização e proteção de mensagens serão abordadas neste artigo, exemplificando a sua utilização e configuração.

Modos de Segurança

Apesar de serviços WCF não fugirem muito do padrão de segurança de aplicações convencionais, há algumas exceções e uma delas é a forma de segurança que será aplicada à mensagem. Essa forma de segurança influencia em como a autenticação será realizada e como a mensagem será protegida durante a sua viagem. É importante dizer que se a transferência da mensagem entre o cliente e o serviço ou entre o serviço e o cliente não fosse protegida, a autenticação e autorização estariam completamente vulneráveis, permitindo vários tipos de ataques.

Assim como várias outras configurações, a segurança também é uma característica do binding, podendo efetuar a configuração de forma declarativa ou imperativa. O WCF fornece cinco formas diferentes para tornar segura a transferência da mensagem. Cada uma dessas formas tem suas particularidades e influenciam em como a mensagem será protegida e como a autenticação será realizada. A tabela abaixo exibe essas cinco formas de segurança, detalhando cada uma delas:

Tipo Descrição
None

Como o próprio nome já indica, nenhuma espécie de segurança é fornecida e toda a mensagem será trafegada sem criptografia.

Transport

Esta opção informa ao WCF que o transporte (TCP, IPC, MSMQ ou HTTPS) irá garantir a segurança da mensagem, criptografando toda a comunicação e, além disso, fornece integridade, privacidade e autenticação mútua. Um dos pontos negativos deste modo é que a segurança será apenas garantida ponto-a-ponto, ou seja, se houver intermediários entre o cliente e o serviço não teremos a garantia de que a mensagem chegará segura até o destino final.

Message

Com esta opção toda a mensagem será criptografada, garantindo assim a autenticação mútua e a proteção da mensagem (confidencialidade e integridade). Ao contrário da segurança baseada no transporte, a segurança a nível de mensagem, garante a segurança end-to-end, independentemente de quantos intermediários houver entre o cliente e o serviço, permintindo inclusive que o serviço seja exposto sob um protocolo não seguro, como é o caso do HTTP. Outro grande benefício que existe é que a segurança é baseada em padrões existentes no mercado, o que garantirá a interoperabilidade. Já um ponto negativo desta opção é o overhead que existe, pois toda e qualquer mensagem será criptografada e assinada.

Both

Como o próprio nome diz, esta opção utiliza a segurança a nível de transporte e a nível de mensagem, ou seja, a mensagem será protegida e, além disso, será transferida por um transporte seguro. Apesar de maximizar a segurança, isso pode causar uma grande perda de performance e, atualmente, é somente permitido em protocolos específicos, como é o caso do Message Queue, onde a latência não é sentida.

TransportWithMessageCredential

Esta opção é um mix das duas anteriores, ou seja, a autenticação do cliente será fornecida a nível de mensagem enquanto a proteção da mensagem (confidencialidade e integridade) e a autenticação do serviço serão fornecidas pela segurança do transporte.

TransportCredentialOnly

Apenas a autenticação mútua é fornecida a nível de transporte, não havendo a proteção da mensagem. Esta opção somente está disponível para o binding basicHttpBinding.

A listagem acima possui todas as opções disponíveis, mas isso não quer dizer que todos os bindings fornecidos pelo WCF suportam todas elas. Todos os bindings oferecem uma propriedade chamada Security e, a partir dela, temos uma segunda propriedade chamada Mode que, como já podemos notar, especificará um dos tipos listados na tabela acima através de um enumerador exclusivo.

Para ter uma visão mais detalhada de quais modos de segurança cada binding suporta e, principalmente, qual é a configuração padrão, a tabela abaixo sumariza essas informações, incluindo o tipo do enumerador utilizado por cada binding para determinar o modo desejado (sendo as opções em negrito a configuração padrão):

Binding None Transport Message Mixed Both Enumerador
BasicHttpBinding Sim Sim Sim Sim Não BasicHttpSecurityMode
WebHttpBinding Sim Sim Não Sim Não WebHttpSecurityMode
WSHttpBinding Sim Sim Sim Não Não SecurityMode
WSDualHttpBinding Sim Não Sim Sim Não WSDualHttpSecurityMode
WSFederationHttpBinding Sim Não Sim Sim Não WSFederationHttpSecurityMode
NetTcpBinding Sim Sim Sim Sim Não SecurityMode
NetPeerTcpBinding Sim Sim Sim Sim Não SecurityMode
NetNamedPipeBinding Sim Sim Não Não Não NetNamedPipeSecurityMode
NetMsmqBinding Sim Sim Sim Não Sim NetMsmqSecurityMode
MsmqIntegrationBinding Sim Sim Não Não Não MsmqIntegrationSecurityMode

Uma vez que conhecemos os modos de segurança que temos e que é o binding quem determinará qual deles poderá ser utilizado, chega o momento de saber como devemos proceder para efetuar essa configuração no serviço. Essa configuração poderá ser realizada de forma imperativa ou declarativa. Os bindings fornecem a possibilidade de configurar a segurança a nível de transporte e de mensagem através de propriedades diferentes e, obviamente, que devemos recorrer a uma delas, dependendo do modo escolhido. Os dois trechos de código a seguir ilustram como fazer para efetuar tal configuração de forma imperativa e, na sequência, de forma declarativa:

using System;
using System.ServiceModel;

WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);

//ou

WSHttpBinding binding = new WSHttpBinding();
binding.Security.Mode = SecurityMode.Message;
Imports System
Imports System.ServiceModel

Dim binding As New WSHttpBinding(SecurityMode.Message)

"ou

Dim binding As New WSHttpBinding()
binding.Security.Mode = SecurityMode.Message
C# VB.NET

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="srvBinding">
          <security mode="Message">
            <!-- outras configurações -->
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>
*.Config

Quando a segurança está habilitada, há uma classe chamada ServiceSecurityContext que representa o contexto de segurança. Se ela estiver sendo acessada no serviço essa classe representará o cliente, e se estiver sendo acessada no cliente representará o serviço. Essa classe fornece uma propriedade estática chamada Current que retorna o contexto atual. As configurações de segurança de um binding e as informações fornecidas pela classe ServiceSecurityContext não param por aí. Vamos ainda explorar vários outros detalhes durante o decorrer deste artigo.

Autenticação

O processo de autenticação consiste em certificarmos que o cliente é quem ele diz ser e, indo mais além, também temos que certificar o serviço. Para efetuar o processo de autenticação, o cliente precisará informar as suas credenciais para que o serviço consiga autenticá-lo. Para isso, o WCF disponibiliza várias possibilidades, e cada uma delas atenderá um cenário específico. A tabela abaixo lista todas essas possibilidades, incluindo uma breve descrição de cada uma delas:

Tipo Descrição
None

Nenhuma espécie de autenticação é realizada.

Windows

Utiliza a autenticação baseada no Windows, fazendo com que o cliente forneça um token, representando sua credencial. Quando houver um domínio o protocolo Kerberos será utilizado, caso contrário, será utilizado o NTLM.

UserName

Sendo uma das formas mais comuns, esta opção possibilita ao WCF trabalhar com um usuário e senha a serem informados pelo cliente antes de invocar a operação, validando o mesmo no próprio Windows ou em alguma tabela do banco de dados.

Certificate

Permite ao cliente apresentar um certificado que o identifica.

IssuedToken

Geralmente utilizado em serviços federados (serão abordados mais abaixo), issued tokes são tokens gerados por um serviço específico e, futuramente, podemos encaminhar este token para as aplicações que exigem a autenticação. Como há uma relação de confiança entre o serviço que gera o token e a aplicação, então o usuário está autenticado.

Custom

Possibilita a criação customizada de um autenticador.

Para utilizar uma das opções, primeiramente é necessário analisar qual dos modos de segurança (transporte ou mensagem) você irá utilizar, pois nem todas as opções acima estão disponíveis em ambos os modos. Antes de falarmos sobre a implementação, é importante entender a relação dos tipos de credenciais e o modo de segurança e, para isso, as tabelas abaixo exibem a relação entre os modos de segurança e quais meios de fornecimento de identidade são suportados por eles, lembrando que são os bindings que expõem as propriedades utilizadas para tal configuração (em negrito é a configuração padrão):

Relação entre os bindings e a segurança baseada no transporte
Binding None Windows UserName Certificate
BasicHttpBinding Sim Sim Sim Sim
WebHttpBinding Sim Sim Sim Sim
WSHttpBinding Sim Sim Sim Sim
WSDualHttpBinding -- -- -- --
WSFederationHttpBinding -- -- -- --
NetTcpBinding Sim Sim Não Sim
NetPeerTcpBinding Não Não Sim Sim
NetNamedPipeBinding Não Sim Não Não
NetMsmqBinding Sim Sim Não Sim
MsmqIntegrationBinding Sim Sim Não Sim

Relação entre os bindings e a segurança baseada na mensagem
Binding None Windows UserName Certificate IssuedToken
BasicHttpBinding Não Não Sim Sim Não
WebHttpBinding -- -- -- -- --
WSHttpBinding Sim Sim Sim Sim Sim
WSDualHttpBinding Sim Sim Sim Sim Sim
WSFederationHttpBinding -- -- -- -- --
NetTcpBinding Sim Sim Sim Sim Sim
NetPeerTcpBinding -- -- -- -- --
NetNamedPipeBinding -- -- -- -- --
NetMsmqBinding Sim Sim Sim Sim Sim
MsmqIntegrationBinding -- -- -- -- --

Cada binding fornece uma propriedade chamada ClientCredencialType que está acessível a partir da propriedade Transport ou Message, onde utilizaremos uma das duas, de acordo com o modo (proteção) de transferência escolhido. Para ilustrar essa configuração, os trechos de códigos abaixo exibem a forma imperativa e declarativa:

using (ServiceHost host = new ServiceHost(typeof(Servico), 
    new Uri[] { new Uri("http://localhost:9922")}))
{
    WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);
    binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;

    host.Open();
}
Using host As New ServiceHost(GetType(Servico), _
    New Uri() { New Uri("http://localhost:9922") })

    Dim binding As New WSHttpBinding(SecurityMode.Message)
    binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName

    host.Open()
End Using
C# VB.NET

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="ServiceBinding">
          <security mode="Message">
            <message clientCredentialType="UserName" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>
*.Config

function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i Para efetuar a configuração específica de um dos tipos de autenticação no serviço, devemos recorrer a uma classe chamada ServiceCredentials (namespace System.ServiceModel.Description). Essa classe fornece propriedades específicas para cada tipo de autenticação (como por exemplo: IssuedTokenAuthentication, PeerCredential, UserNameAuthentication, WindowsAuthentication), onde devemos utilizar uma delas, de acordo com o tipo de autenticação a ser realizado. Essa classe ainda implementa a Interface IServiceBehavior e, com isso, deve ser adicionada como um behavior do serviço. Abaixo é mostrado como efetuar a configuração de um serviço para que o mesmo utilize a autenticação baseada em UserName:

using (ServiceHost host = new ServiceHost(typeof(Servico), 
    new Uri[] { new Uri("http://localhost:2999/") }))
{
    WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);

    ServiceCredentials credenciais = new ServiceCredentials();
    credenciais.UserNameAuthentication.UserNamePasswordValidationMode = 
        UserNamePasswordValidationMode.Custom;
    credenciais.UserNameAuthentication.CustomUserNamePasswordValidator = 
	    new Autenticador();
    credenciais.ServiceCertificate.SetCertificate(
        StoreLocation.LocalMachine,
        StoreName.My,
        X509FindType.FindBySerialNumber,
        "3c f2 d0 c0 ef 8e 0a 96 42 36 e6 54 5f 67 50 e0");

    host.Description.Behaviors.Add(credenciais);

    host.AddServiceEndpoint(typeof(IContrato), binding, "srv");

    host.Open();
    Console.ReadLine();
}
Using host As New ServiceHost(GetType(Servico), _
    New Uri() { New Uri("http://localhost:2999/") }))
{
    Dim binding As New WSHttpBinding(SecurityMode.Message)

    ServiceCredentials credenciais = new ServiceCredentials()
    credenciais.UserNameAuthentication.UserNamePasswordValidationMode = _
        UserNamePasswordValidationMode.Custom
    credenciais.UserNameAuthentication.CustomUserNamePasswordValidator = new Autenticador()
    credenciais.ServiceCertificate.SetCertificate(
        StoreLocation.LocalMachine,
        StoreName.My,
        X509FindType.FindBySerialNumber,
        "3c f2 d0 c0 ef 8e 0a 96 42 36 e6 54 5f 67 50 e0")

    host.Description.Behaviors.Add(credenciais)

    host.AddServiceEndpoint(GetType(IContrato), binding, "srv")

    host.Open()
    Console.ReadLine()
End Using
C# VB.NET

Quando utilizamos a autenticação baseada em UserName, devemos especificar qual será o modo de autenticação e, para isso, utilizamos a propriedade UserNamePasswordValidationMode que recebe uma das três opções do enumerador UserNamePasswordValidationMode, listadas abaixo:
  • Custom: Permite especificar um validador customizado.

  • Membership: Efetuará a validação baseando-se no ASP.NET.

  • Windows: Os usernames serão mapeados para usuários Windows.

Quando utilizamos a opção Custom devemos especificar a classe que fará a validação. Para isso, devemos definir a propriedade CustomUserNamePasswordValidator com a instância da classe validadora que, obrigatoriamente, deverá herdar da classe UserNamePasswordValidator, sobrescrevendo o método Validate, assim como mostrado abaixo:

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

public class Autenticador : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        if (userName != "Israel" || password != "123")
            throw new SecurityTokenException("Credenciais Invalidas.");
    }
}
Imports System
Imports System.IdentityModel.Tokens
Imports System.IdentityModel.Selectors

Public Class Autenticador
    Inherits UserNamePasswordValidator

    Public Overrides Sub Validate(ByVal userName As String, ByVal password As String)
        If Not userName = "Israel" OrElse Not password = "123" Then
            Throw New SecurityTokenException("Credenciais Invalidas.")
        End If
    End Sub
End Class
C# VB.NET

Observação: 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.

Quando o serviço é exposto utilizando algum meio de autenticação que vimos acima, ao referenciar o mesmo em um cliente, será necessário que este cliente forneça as credenciais para o serviço que, consequentemente, irá validar o cliente em algum repositório. Quando criamos o proxy, a classe que o representa herda diretamente da classe ClientBase<TChannel> que, por sua vez, disponibiliza uma propriedade chamada ClientCredentials. Essa propriedade retorna uma instância da classe ClientCredentials (namespace System.ServiceModel.Description) que possibilita ao cliente configurar as credenciais, fornecendo propriedades especificadas para cada tipo de autenticação, por exemplo: ClientCertificate, HttpDigest, IssuedToken, Peer, ServiceCertificate, UserName e Windows. Devemos apenas utilizar uma delas, de acordo com a especificação/exigência do serviço.

Como os exemplos de configuração que vimos acima foi utilizando o UserName, o código abaixo ilustra como proceder para efetuar a configuração do lado do cliente, fornecendo explicitamente o username e senha antes de invocar a operação.

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

    Console.WriteLine(proxy.Metodo());
}
Using proxy As New ContratoClient()
    proxy.ClientCredentials.UserName.UserName = "Israel"
    proxy.ClientCredentials.UserName.Password = "123"

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

A classe ServiceSecurityContext é responsável por disponibilizar informações a respeito do contexto de segurança da requisição atual. As principais propriedades que ela fornece para extrair informações a respeito do usuário autenticação são: PrimaryIdentity e WindowsIdentity. Ambas refletem a identidade do usuário autenticado, mas a segunda somente estará acessível caso a autenticação seja baseada no Windows. Caso o usuário não estiver autenticado, a propriedade PrimaryIdentity retornará uma identidade do tipo GenericIdentity em branco. Abaixo o exemplo de como acessar essas propriedades:

ServiceSecurityContext ctx = OperationContext.Current.ServiceSecurityContext;

Debug.WriteLine(string.Format("PrimaryIdentity: {0}", ctx.PrimaryIdentity));
Debug.WriteLine(string.Format("WindowsIdentity: {0}", ctx.WindowsIdentity));
Dim ctx As ServiceSecurityContext = OperationContext.Current.ServiceSecurityContext

Debug.WriteLine(string.Format("PrimaryIdentity: {0}", ctx.PrimaryIdentity))
Debug.WriteLine(string.Format("WindowsIdentity: {0}", ctx.WindowsIdentity))
C# VB.NET

Autorização

Acontecendo sempre depois da autenticação, a autorização verificará quais direitos um determinado indivíduo tem sobre o sistema. Uma vez que sabemos quem ele é, então é necessário conhecermos também quais privilégios ele tem no sistema e, a partir daí, conceder ou negar acesso à um determinado recurso. Para gerenciar isso, o WCF fornece três diferentes técnicas, a saber:

Tipo Descrição
Role-based

Neste caso o acesso às operações são controlados por papéis, ou seja, o usuário (cliente) somente conseguirá ter acesso se o mesmo estiver dentro do respectivo papel. Como repositório podemos utilizar os grupos do Windows, o RoleProvider do ASP.NET ou até mesmo um repositório customizado, como é o caso de um banco de dados exclusivo.

Identity-based

Conhecido como Identity Model, este modo suporta um novo modelo de autorização baseado em claims, onde você deverá analisar cada claim que estará dentro das credenciais do usuário autenticado e, assim, determinar se o usuário tem ou não acesso.

Resource-based

Com esta opção os recursos são protegidos a partir das ACLs do Windows (Access Control Lists). Esta opção geralmente obriga o serviço a fazer o impersonation ou delegation (mais detalhes abaixo) do cliente antes de acessar o recurso, ficando sob responsabilidade do próprio sistema operacional verificar se o cliente tem ou não permissão de acesso.

Vimos no primeiro modo, role-based, que podemos fazer a utilização de vários repositórios, como é o caso do ASP.NET 2.0, dos grupos do Windows ou customizado. Neste caso, utilizamos as técnicas existentes dentro do .NET Framework desde sua primeira versão, onde basicamente recorremos às classes que implementam as Interfaces IIdentity e IPrincipal. A segunda gerencia a autorização expondo um método chamado IsInRole que, dado um papel, retorna um valor boleano indicando se o usuário corrente faz ou não parte do mesmo. Quando trabalhamos com credenciais Windows, então temos a classe WindowsPrincipal; já quando tratam-se de outros repositórios, como é o caso do ASP.NET, então utilizaremos a classe GenericPrincipal para o gerenciamento da autorização. Para mais detalhes sobre esses tipos, consulte o capítulo 9 deste artigo.

O primeiro passo é a configuração da autorização. Essa configuração será definida a partir de um behavior de serviço do tipo ServiceAuthorizationBehavior que irá criar e gerenciar o repositório dos papéis. Este behavior disponibiliza uma propriedade chamada PrincipalPermissionMode que aceita uma das quatro opções expostas pelo enumerador PrincipalPermissionMode e que especificará qual deles utilizar:

  • Custom: Opção que permite especificar um repositório de papéis customizado. Neste caso, a classe que representará este repositório deverá obrigatoriamente implementar a Interface IPrincipal.

  • None: A autorização baseada em papéis está desabilitada, populando a propriedade CurrentPrincipal com uma identidade em branco.

  • UseAspNetRoles: Nesta opção o repositório a ser utilizado será o RoleProvider do ASP.NET.

  • UseWindowsGroups: Utiliza os grupos do Windows para efetuar a autorização. Este é o valor padrão quando não especificado.

A configuração de qual dos tipos utilizar pode, como várias outras configurações, ser definida de forma imperativa ou declarativa, lembrando que ela deve ser configurada antes da abertura do host. Os códigos abaixo exibem como efetuar essa configuração:

using (ServiceHost host = new ServiceHost(typeof(Servico), 
    new Uri[] { new Uri("net.tcp://localhost:9922")}))
{
    host.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.UseAspNetRoles;

    host.Open();
}
Using host As New ServiceHost(GetType(Servico), _
    New Uri() { New Uri("net.tcp://localhost:9922") })

    host.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.UseAspNetRoles

    host.Open()
End Using
C# VB.NET

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="DefaultService" behaviorConfiguration="ServiceBehavior">
          <!-- Endpoints -->
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehavior">
          <serviceAuthorization principalPermissionMode="UseAspNetRoles" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>
*.Config

Uma vez que a configuração do repositório da autorização já está definida, chega o momento de especificarmos qual usuário ou qual papel tem acesso à uma determinada operação ou recurso. Mais uma vez, temos a possibilidade de aplicar a autorização de forma imperativa e declarativa mas, neste caso, os modos são utilizados para diferentes técnicas. O modo declarativo definirá a autorização para o método todo, enquanto o modo imperativo nos dará uma maior flexibilidade, já que podemos analisar se o usuário pertence ou não a algum papel e, consequentemente, conceder o acesso. No código abaixo primeiramente verificamos se o usuário é um administrador e, caso seja, permite o acesso a algum recurso protegido e, como dito anteriormente, podemos silenciosamente efetuar alguma operação sem disparar exceções caso ele não faça parte deste grupo.

using System;
using System.ServiceModel;

public class Servico : IContrato
{
    public string Metodo()
    {
        if (Thread.CurrentPrincipal.IsInRole("Admin"))
        {
            //faça algo
        }
    }
}
Imports System
Imports System.ServiceModel

Public Class Servico
    Implements IContrato

    Public Function Metodo() As String
        If Thread.CurrentPrincipal.IsInRole("Admin") Then
            "faça algo
        End If
    End Function
End Class
C# VB.NET

No modo declarativo utilizamos um velho conhecido: o atributo PrincipalPermissionAttribute que faz parte do namespace System.Security.Permissions. Este atributo fornece uma propriedade chamada Role onde podemos especificar o papel necessário para que o usuário tenha o direito de executar o método. Caso mais de um papel possa ter o direito de acesso ao método, então você poderá aplicar múltiplas vezes o mesmo atributo no método, variando apenas o nome do papel, assim como é mostrado abaixo:

using System;
using System.ServiceModel;

public class Servico : IContrato
{
    [PrincipalPermission(SecurityAction.Demand, Role = "Admin")]
    [PrincipalPermission(SecurityAction.Demand, Role = "Gerentes")]
    public string Metodo()
    {
        //faça algo
    }
}
Imports System
Imports System.ServiceModel

Public Class Servico
    Implements IContrato

    <PrincipalPermission(SecurityAction.Demand, Role:="Admin"), _
     PrincipalPermission(SecurityAction.Demand, Role:="Gerentes")> _
    Public Function Metodo() As String
        "faça algo
    End Function
End Class
C# VB.NET

Caso o usuário não faça parte de nenhum dos papéis especificados no atributo, então uma exceção do tipo SecurityException será disparada. Esse atributo funciona com qualquer modo de autorização, mas precisamos nos atentar com alguns detalhes. Quando utilizamos o ASP.NET, basicamente colocamos o nome do papel; já quando utilizamos o Windows, então é necessário especificar o domínio antes do papel, ficando algo como: "Domínio\Admin".

A integração com o RoleProvider do ASP.NET permite aos serviços WCF fazerem uso de uma arquitetura existente e fornecida pelo ASP.NET. O recurso de Provider Model do ASP.NET foi disponibilizado a partir da versão 2.0, trazendo vários recursos que utilizam esta técnica, como é o caso do MembershipProvider (falado acima) e do RoleProvider. Para entender detalhadamente como funciona essa integração, consulte este artigo.

Para finalizar esta seção, o modo Identity-based fornece um conjunto de APIs que gerencia os claims e as policies e, baseado nestas claims, concedem ou negam o acesso. Este recurso é comumente utilizado em ambientes federados, que será comentado mais tarde, ainda neste artigo. Já no modo Resource-based, a autorização será analisada pelo próprio sistema operacional, baseando-se em ACLs previamente configuradas.

Impersonation/Delegation

O conceito de impersonation já existe há algum tempo e não é exclusividade do WCF. Esta técnica permite à aplicação assumir a identidade do usuário para atuar como ele durante o acesso a alguns recursos, como sistema de arquivos e banco de dados que estão disponíveis na mesma máquina onde está sendo executado o serviço. Já o conceito de delegation é usado para acessar recursos que estão remotos, ou melhor, fora da máquina onde está o serviço.

Podemos controlar o impersonation nas duas extremidades, ou seja, do lado do serviço e do lado do cliente. Do lado do serviço podemos recorrer à propriedade Impersonation do atributo OperationBehaviorAttribute, especificando uma das três opções expostas pelo enumerador ImpersonationOption:

  • NotAllowed: O impersonation não será executado em uma operação específica. Quando não informado, esta é a opção padrão.

  • Allowed: Caso um token Windows esteja disponível e a propriedade ImpersonateCallerForAllOperations da classe ServiceAuthorizationBehavior esteja definida como True (que por padrão é definida como False), o impersonation será executado.

  • Required: Obriga o cliente a fornecer um token Windows para efetuar o impersonation, disparando uma exceção caso o mesmo não seja informado.

O código a seguir exibe como podemos configurar uma determinada operação para requerer o impersonation. Note que por se tratar de um behavior, isso deverá ser aplicado na classe que implementa a Interface do contrato:

using System;
using System.ServiceModel;

public class Servico : IContrato
{
    [OperationBehavior(Impersonation = ImpersonationOption.Required)]
    public string Metodo()
    {
        //Implementação        
    }
}
Imports System
Imports System.ServiceModel

Public Class Servico
    Implements IContrato

    <OperationBehavior(Impersonation:=ImpersonationOption.Required)> _
    Public Function Metodo() As String
        "Implementação        
    End Function
End Class
C# VB.NET

Como vimos, a opção Allowed somente faz sentido quando você desejar "ligar" ou "desligar" o impersonation (apesar de que isso não seja uma boa prática). Quando definido como Allowed o WCF sempre irá respeitar o valor especificado na propriedade ImpersonateCallerForAllOperations, que pode ser definida de forma imperativa ou declarativa, tendo o cuidado de definir este valor antes da abertura do host. O código abaixo ilustra essas duas possíveis configurações:

using (ServiceHost host = new ServiceHost(typeof(Servico), 
    new Uri[] { new Uri("net.tcp://localhost:9922")}))
{
    host.Authorization.ImpersonateCallerForAllOperations = true;

    host.Open();
}
Using host As New ServiceHost(GetType(Servico), _
    New Uri() { New Uri("net.tcp://localhost:9922") })

    host.Authorization.ImpersonateCallerForAllOperations = True

    host.Open()
End Using
C# VB.NET

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Servico" behaviorConfiguration="behaviorConfig">
        <!-- outras configurações -->
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="behaviorConfig">
          <serviceAuthorization impersonateCallerForAllOperations="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>
*.Config

Acima foi falado que também podemos controlar a impersonation do lado do cliente. A idéia aqui é proteger o cliente de serviços maliciosos, que pode capturar a sua credencial e executar uma operação ilegal ou que danifique algum recurso. Pode ser que em alguns casos você não queira possibilitar que o cliente forneça a sua identidade para serviço e, para customizar isso, podemos utilizar o enumerador TokenImpersonationLevel (contido no namespace System.Security.Principal), que fornece as seguintes opções:

  • None: O impersonation não será permitido.

  • Anonymous: O serviço não poderá obter a identificação do cliente e, consequentemente, não poderá efetuar o impersonation para o cliente corrente.

  • Identification: O serviço poderá obter a identificação do cliente, mas não poderá efetuar o impersonation e, se tentar efetuar, uma exceção será disparada.

  • Impersonation: Além de obter a identificação do cliente, poderá também efetuar o impersonation do cliente, mas podendo acessar somente os recursos que estão rodando na mesma máquina do serviço.

  • Delegation: Pode efetuar o impersonation do cliente e também acessar recursos que estão além da máquina onde está o serviço.

function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i Como essa é uma configuração que diz respeito ao cliente, é do lado do mesmo que devemos efetuá-la, podendo ser de forma declarativa ou imperativa. Como isso é uma configuração exclusiva para quando a autenticação é baseada no Windows, é através desta opção que iremos acessar a propriedade AllowedImpersonationLevel que aceita uma das opções expostas pelo enumerador que vimos acima:

using System.Security.Principal;

using (ContratoClient proxy = new ContratoClient())
{
    proxy.ClientCredentials.Windows.AllowedImpersonationLevel = 
        TokenImpersonationLevel.Delegation;

    //chamada para as operações
}
Imports System.Security.Principal

Using proxy As New ContratoClient()
    proxy.ClientCredentials.Windows.AllowedImpersonationLevel = _
        TokenImpersonationLevel.Delegation

    "chamada para as operações
End Using
C# VB.NET

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint name="srv" behaviorConfiguration="edpBehavior" ...>
        <!-- outras configurações -->
      </endpoint>
    </client>
    <behaviors>
      <endpointBehaviors>
        <behavior name="edpBehavior">
          <clientCredentials>
            <windows allowedImpersonationLevel="Delegation"/>
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>
*.Config

Observação: Quando optamos por habilitar o impersonation de forma declarativa, o método todo será executado sob as credenciais do cliente, revertendo para a identidade original quando o método é retornado. Se quisermos ter um controle mais refinado sobre isso, então podemos utilizar o método Impersonate da classe WindowsIdentity, que nos permite efetuar o impersonation de forma imperativa. Quando utilizamos esta técnica, devemos utilizar o método Undo desta mesma classe para reverter a identidade.

Integridade e Confidencialidade

Felizmente o WCF fornece aos desenvolvedores a possibilidade de ter integridade e confidencialidade para o envio de mensagens. A integridade consiste em ninguém conseguir mudar o conteúdo da mensagem durante a sua viagem e, caso haja, o WCF conseguirá detectar e descartar a mesma. Já com a confidencialidade temos a garantia de que ninguém, com exceção do destinatário, conseguirá ler o que está definido no interior da mensagem. É importante dizer que a integridade não garante a confidencialidade, ou seja, a integridade apenas determina que o conteúdo não será modificado entre o cliente e o serviço, mas ele continuará legível.

Para controlar essas funcionalidades o WCF fornece dois mecanismos, onde um deles pode ser aplicado a uma operação específica enquanto o outro pode ser aplicado ao contrato como um todo. Escolher uma das alternativas dependerá do que você precisa proteger, ou seja, não vale a pena proteger todas as operações do contrato se várias delas não expõem nenhuma informação confidencial. Para ambas as alternativas podemos utilizar uma das opções expostas pelo enumerador ProtectionLevel, que está contido no namespace System.Net.Security. Esse enumerador fornece três opções:

  • None: Desabilita qualquer tipo de proteção na mensagem.

  • Sign: Apenas assina a mensagem, não criptografando-a. Essa opção irá garantir a integridade, enquanto o seu conteúdo continua legível.

  • EncryptAndSign: Criptografa e em seguida assina a mensagem, garantindo a integridade e confidencialidade.

Qualquer uma das opções escolhidas apenas afetam os dados da aplicação, ou seja, informações da infraestrutura, headers, etc., não serão impactadas. Outro detalhe importante é que quando o modo de segurança é definido como transporte, a mensagem toda será protegida pelos mecanismos fornecidos pelo transportante, não tendo qualquer efeito o uso do ProtectionLevel.

Como falado, você pode aplicar essa técnica a uma operação ou ao contrato todo e, para isso, temos a nossa disposição uma propriedade chamada ProtectionLevel, acessível através dos atributos que são utilizados para compor a mensagem e que estão em diferentes escopos: OperationContractAttribute, ServiceContractAttribute, FaultContractAttribute, MessageContractAttribute, MessageBodyContractAttribute e MessageHeaderAttribute. Vejamos através do trecho de código abaixo como podemos proceder para proteger todo o contrato:

using System;
using System.ServiceModel;
using System.Net.Security;

[ServiceContract(ProtectionLevel = ProtectionLevel.Sign)]
public interface IContrato
{
    [OperationContract]        
    string Metodo();
}
Imports System
Imports System.ServiceModel
Imports System.Net.Security

<ServiceContract(ProtectionLevel:=ProtectionLevel.Sign)> _
Public Interface IContrato
    <OperationContract> _     
    Function Metodo() As String
End Interface
C# VB.NET

O uso desta técnica também está relacionada com o binding. Se você define explicitamente esta configuração, o binding obrigatoriamente deverá estar com a segurança habilitada. Caso você não defina explicitamente o ProtectionLevel e tiver a segurança habilitada, o WCF sempre utilizará a configuração padrão que é EncryptAndSign (salvo os bindings que não suportam segurança a nível de mensagem, como é o caso do basicHttpBinding).

Outro detalhe importante quando estamos trabalhando com criptografia é o algoritmo a ser utilizado. O WCF pode fazer uso de vários algoritmos existentes mas é importante dizer que os algoritmos devem ser iguais no serviço e no cliente para que a criptografia funcione sem problemas. Esta também é uma configuração que está debaixo do binding e deve ser especificada de forma declarativa ou imperativa. No modo declarativo, o atributo algorithmSuite recebe uma string que especifica o algoritmo, enquanto que no modo imperativo, a propriedade AlgorithmSuite espera uma instância da classe SecurityAlgorithmSuite, representando o algoritmo escolhido. No modo imperativo, utilizamos uma das propriedades estáticas disponibilizadas por esta mesma classe que retorna a instância da mesma configurada com o algoritmo escolhido. Os trechos abaixo ilustram ambas as formas de configuração do algoritmo de criptografia:

WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);
binding.Security.Message.AlgorithmSuite = SecurityAlgorithmSuite.TripleDes;
Dim binding As New WSHttpBinding(SecurityMode.Message)
binding.Security.Message.AlgorithmSuite = SecurityAlgorithmSuite.TripleDes
C# VB.NET

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="BindingConfig">
          <security mode="Message">
            <message algorithmSuite="TripleDes" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>
*.Config

Negociação

A "negociação" é uma funcionalidade que permite ao cliente e ao serviço negociar uma chave para efetuar a autenticação mútua e a proteção da mensagem quando o serviço está definido com a segurança baseada na mensagem. Na segurança baseada no transporte essa negociação é feita de forma automática e não conseguimos interferir neste processo.

Quando ela estiver habilitada, que é o padrão, ela elimina a necessidade do cliente conhecer antecipadamente a chave necessária para efetuar a troca das mensagens, ao contrário de quando ela estiver desabilitada, que nos obriga a ter alguns cuidados, como por exemplo: se o serviço espera por credenciais Windows, então tanto o cliente quanto o serviço devem estar dentro de um mesmo domínio; já em outras formas de autenticação, como UserName (que exige um certificado), obrigará o cliente a extrair a chave pública do certificado de uma outra forma, já que não haverá a negociação.

Há possibilidade de configurar este recurso tanto de forma declarativa quanto imperativa. No modo imperativo, recorremos à propriedade boleana NegotiateServiceCredential da classe NonDualMessageSecurityOverHttp, que estará acessível através da propriedade Security do binding. Já no modo declarativo temos o atributo boleano negotiateServiceCredential exposto através do elemento security, como é mostrado no trecho de código abaixo:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="BindingConfig">
          <security mode="Message">
            <message clientCredentialType="UserName" negotiateServiceCredential="false" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>
*.Config

Sessões Seguras

Quando uma determinada operação é invocada, o processo de autenticação sempre será realizado, verificando se o usuário é quem ele diz ser. Se, a partir de um mesmo proxy eu invoque N operações, a autenticação ocorrerá N vezes. Visando diminuir este overhead, o WCF disponibiliza um recurso chamado de sessões seguras. Em lugares onde você precisa invocar várias operações, a criação de uma sessão segura melhora consideravelmente a performance, evitando que a autenticação aconteça toda vez.

Ao invocar a primeira operação, automaticamente a sessão segura será estabelecida entre o cliente e o serviço e, para identificá-la é criado um security context token (SCT). Com isso, todas as requisições subsequentes irão utilizar este token ao invés de criar uma nova sessão e/ou refazer a autenticação. É importante dizer que, por padrão, esta sessão (token) tem 15 minutos de vida, sendo descartada se nenhuma operação for invocada neste intervalo de tempo.

Basicamente, todos os bindings que suportam a segurança baseada em mensagem (e que a utilizam) também podem fazer uso das sessões seguras. Há alguns bindings que não tem a segurança em nível de mensagem como padrão, sendo o binding NetTcpBinding um exemplo disso. Apenas o fato de definí-lo com segurança em nível de mensagem, automaticamente ele criará a sessão segura, não permitindo desabilitar este recurso. Já os outros bindings, como o WSHttpBinding, nos permitem desabilitar este recurso, através do atributo boleano establishSecurityContext, do elemento security. O trecho de código abaixo ilustra como podemos proceder para desabilitá-lo:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="BindingConfig">
          <security mode="Message">
            <message establishSecurityContext="false" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>
*.Config

Há também a possibilidade de configurar esta funcionalidade via modo imperativo. Para isso, basta recorrermos à propriedade (também boleana) EstablishSecurityContext da classe NonDualMessageSecurityOverHttp, que estará acessível através da propriedade Security do binding.

Como falado acima, o serviço cria um SCT para o cliente e, se no intervalo entre o retorno do serviço com o SCT criado e a próxima requisição o host for reinicializado (muito comum quando o serviço é exposto através do IIS), uma exceção do tipo MessageSecurityException será lançada. Isso ocorre porque do lado do serviço há uma chave que é armazenada na memória do processo e que está associada com o SCT do cliente e, uma vez perdido, ele não será mais válido.

Esse comportamento, que é o padrão, é conhecido como Stateless SCT e pode ser substituído por um Stateful SCT e, neste caso, a chave que antes ficava na memória do serviço, passa a ficar embutida no próprio SCT que é enviado para o cliente e, consequentemente, sobrevivendo às possíveis reinicializações do serviço.

Federation

Todas as configurações da autenticação e da autorização que vimos no decorrer deste artigo são realizadas em cima do serviço que estamos criando para atender uma determinada regra de negócio. Muitas vezes, ao criar um novo serviço para a mesma empresa, cria-se e configura-se novamente toda a parte de autenticação e autorização, e as vezes se tem duplicação de usuários, senhas, etc., dificultando o gerenciamento por parte da aplicação e também do usuário que a acessa.

O conceito de federação torna esse gerenciamento e segurança muito mais simples, seguro e eficaz. Os serviços apenas devem focar no desenvolvimento e execução da regra do negócio e a parte de autenticação e autorização será desacoplada do mesmo. Em um ambiente "federado", o cliente (conhecido como Subject) deverá ser autenticado em um provedor de identidades (Security Token Service (STS)) e, finalmente, o token gerado será encaminhado para a aplicação (Relying Party (RP)).

Um outro grande benefício que temos ao utilizar os serviços "federados" é a possibilidade de estabelecer uma relação de confiança entre todos os participantes (RP), criando assim uma espécie de single sign-on (SSO). Isso quer dizer que precisamos nos autenticar uma única vez e ter acesso a todas as aplicações (RPs) que fazem parte, ou melhor, confiam em um provedor (STS) específico. Como há vários mecanismos de autenticação/autorização, o STS irá abstrair isso e as aplicações participantes passam a lidar apenas com os claims, sem a necessidade de conhecer a tecnologia utilizada para autenticação/autorização. A imagem abaixo ilustra supercialmente como este processo acontece:

Figura 1 - Participantes de serviços "federados".

  • Fase 1: A fase 1 consiste em ir até o provedor de identidades e requerer pelo token que identifica o respectivo cliente no ambiente federado.

  • Fase 2: Se o cliente for encontrado, um token é gerado e enviado ao cliente.

  • Fase 3: A partir deste momento todas as requisições para a aplicação (ou serviço) serão realizadas de forma segura, contendo o token gerado pelo provedor que todos confiam.

  • Fase 4: Depois do token validado o serviço permite a execução da requisição e retorna a resposta para o cliente que a solicitou.

"Geneva"

Atualmente conhecido como "Geneva", é o codename para um novo framework que a Microsoft está criando para facilitar a criação de aplicações e serviços baseados em claims e para implementação de serviço "federados". Os recursos fornecidos por esta nova API vai desde a possibilidade de que aplicações (ASP.NET, Windows, etc.) ou serviços possam utilizar o modelo baseado em claims até a construção de nossos próprios STS.

Este framework está dividido em três grandes partes: “Geneva” Server, CardSpace “Geneva” e “Geneva” Framework. Infelizmente não há espaço para falar sobre essa API neste momento, pois ela precisa de um artigo exclusivo para abordar os problemas conhecidos e como ela pode ser utilizada para sanar cada um deles.

Conclusão: Apesar de um pouco grande, este artigo tentou cobrir as principais funcionalidades fornecidas pelo WCF para o gerenciamento de autenticação e autorização de serviços, abordando as vantagens e desvantagens de cada modo. Analisamos outras características ligadas diretamente à autenticação, como foi o caso do impersonation e o delegation e, além disso, vimos como proceder para proteger as mensagens, garantindo a integridade e confidencialidade. Para finalizar, foi falado superficialmente sobre Federation e o "Geneva" que são temas importantes e que serão esgotados em futuros artigos.

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.