Compartilhar via


Tokens com suporte

O exemplo de Tokens com Suporte demonstra como adicionar tokens extras a uma mensagem que usa WS-Security. O exemplo adiciona um token de segurança binário X.509, além de um token de segurança de nome de usuário. O token é passado em um cabeçalho de mensagem WS-Security do cliente para o serviço, e parte da mensagem é assinada com a chave privada associada ao token de segurança X.509 para provar a posse do certificado X.509 para o destinatário. Isso é útil no caso em que há um requisito para ter várias declarações associadas a uma mensagem para autenticar ou autorizar o remetente. O serviço implementa um contrato que define um padrão de comunicação solicitação-resposta.

Demonstra

O exemplo demonstra:

  • Como um cliente pode passar tokens de segurança adicionais para um serviço.

  • Como o servidor pode acessar declarações associadas a tokens de segurança adicionais.

  • Como o certificado X.509 do servidor é usado para proteger a chave simétrica usada para criptografia de mensagem e assinatura.

Observação

O procedimento de instalação e as instruções de compilação dessa amostra estão no final deste tópico.

O cliente se autentica com o token de nome de usuário e dá suporte ao token de segurança X.509

O serviço expõe um único ponto de extremidade para comunicação que é criado programaticamente usando as classes BindingHelper e EchoServiceHost. O ponto de extremidade é composto por um endereço, uma associação e um contrato. A associação é configurada com uma associação personalizada usando SymmetricSecurityBindingElement e HttpTransportBindingElement. Este exemplo define o SymmetricSecurityBindingElement para usar um certificado X.509 de serviço para proteger a chave simétrica durante a transmissão e para passar um UserNameToken junto com o X509SecurityToken de suporte em um cabeçalho de mensagem WS-Security. A chave simétrica é usada para criptografar o corpo da mensagem e o token de segurança de nome de usuário. O token de suporte é passado como um token de segurança binário adicional no cabeçalho da mensagem WS-Security. A autenticidade do token de suporte é provada assinando parte da mensagem com a chave privada associada ao token de segurança X.509 de suporte.

public static Binding CreateMultiFactorAuthenticationBinding()
{
    HttpTransportBindingElement httpTransport = new HttpTransportBindingElement();

    // the message security binding element will be configured to require 2 tokens:
    // 1) A username-password encrypted with the service token
    // 2) A client certificate used to sign the message

    // Instantiate a binding element that will require the username/password token in the message (encrypted with the server cert)
    SymmetricSecurityBindingElement messageSecurity = SecurityBindingElement.CreateUserNameForCertificateBindingElement();

    // Create supporting token parameters for the client X509 certificate.
    X509SecurityTokenParameters clientX509SupportingTokenParameters = new X509SecurityTokenParameters();
    // Specify that the supporting token is passed in message send by the client to the service
    clientX509SupportingTokenParameters.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;
    // Turn off derived keys
    clientX509SupportingTokenParameters.RequireDerivedKeys = false;
    // Augment the binding element to require the client's X509 certificate as an endorsing token in the message
    messageSecurity.EndpointSupportingTokenParameters.Endorsing.Add(clientX509SupportingTokenParameters);

    // Create a CustomBinding based on the constructed security binding element.
    return new CustomBinding(messageSecurity, httpTransport);
}

O comportamento especifica as credenciais de serviço que devem ser usadas para autenticação do cliente, bem como informações sobre o certificado X.509 do serviço. O exemplo usa CN=localhost como um nome de assunto no certificado X.509 de serviço.

override protected void InitializeRuntime()
{
    // Extract the ServiceCredentials behavior or create one.
    ServiceCredentials serviceCredentials =
        this.Description.Behaviors.Find<ServiceCredentials>();
    if (serviceCredentials == null)
    {
        serviceCredentials = new ServiceCredentials();
        this.Description.Behaviors.Add(serviceCredentials);
    }

    // Set the service certificate
    serviceCredentials.ServiceCertificate.SetCertificate(
                                       "CN=localhost");

/*
Setting the CertificateValidationMode to PeerOrChainTrust means that if the certificate is in the Trusted People store, then it will be trusted without performing a validation of the certificate's issuer chain. This setting is used here for convenience so that the sample can be run without having to have certificates issued by a certification authority (CA).
This setting is less secure than the default, ChainTrust. The security implications of this setting should be carefully considered before using PeerOrChainTrust in production code.
*/
    serviceCredentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.PeerOrChainTrust;

    // Create the custom binding and add an endpoint to the service.
    Binding multipleTokensBinding =
         BindingHelper.CreateMultiFactorAuthenticationBinding();
    this.AddServiceEndpoint(typeof(IEchoService),
                          multipleTokensBinding, string.Empty);
    base.InitializeRuntime();
}

Código de serviço:

[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class EchoService : IEchoService
{
    public string Echo()
    {
        string userName;
        string certificateSubjectName;
        GetCallerIdentities(
            OperationContext.Current.ServiceSecurityContext,
            out userName,
            out certificateSubjectName);
            return $"Hello {userName}, {certificateSubjectName}";
    }

    public void Dispose()
    {
    }

    bool TryGetClaimValue<TClaimResource>(ClaimSet claimSet,
            string claimType, out TClaimResource resourceValue)
            where TClaimResource : class
    {
        resourceValue = default(TClaimResource);
        IEnumerable<Claim> matchingClaims =
            claimSet.FindClaims(claimType, Rights.PossessProperty);
        if(matchingClaims == null)
            return false;
        IEnumerator<Claim> enumerator = matchingClaims.GetEnumerator();
        if (enumerator.MoveNext())
        {
            resourceValue =
              (enumerator.Current.Resource == null) ? null :
              (enumerator.Current.Resource as TClaimResource);
            return true;
        }
        else
        {
            return false;
        }
    }

    // Returns the username and certificate subject name provided by
    //the client
    void GetCallerIdentities(ServiceSecurityContext
        callerSecurityContext,
        out string userName, out string certificateSubjectName)
    {
        userName = null;
        certificateSubjectName = null;

       // Look in all the claimsets in the authorization context
       foreach (ClaimSet claimSet in
               callerSecurityContext.AuthorizationContext.ClaimSets)
       {
            if (claimSet is WindowsClaimSet)
            {
                // Try to find a Name claim. This will have been
                // generated from the windows username.
                string tmpName;
                if (TryGetClaimValue<string>(claimSet, ClaimTypes.Name,
                                                      out tmpName))
                {
                    userName = tmpName;
                }
            }
            else if (claimSet is X509CertificateClaimSet)
            {
                // Try to find an X500DistinguishedName claim. This will
                // have been generated from the client certificate.
                X500DistinguishedName tmpDistinguishedName;
                if (TryGetClaimValue<X500DistinguishedName>(claimSet,
                               ClaimTypes.X500DistinguishedName,
                               out tmpDistinguishedName))
                {
                    certificateSubjectName = tmpDistinguishedName.Name;
                }
            }
        }
    }
}

O ponto de extremidade do cliente é configurado de maneira semelhante ao ponto de extremidade de serviço. O cliente usa a mesma classe BindingHelper para criar uma associação. O restante da configuração está localizado na classe Client. O cliente define informações sobre o token de segurança de nome de usuário, o token de segurança X.509 de suporte e informações sobre o certificado X.509 de serviço no código de configuração para a coleção de comportamentos do ponto de extremidade do cliente.

static void Main()
 {
     // Create the custom binding and an endpoint address for
     // the service.
     Binding multipleTokensBinding =
         BindingHelper.CreateMultiFactorAuthenticationBinding();
         EndpointAddress serviceAddress = new EndpointAddress(
         "http://localhost/servicemodelsamples/service.svc");
       ChannelFactory<IEchoService> channelFactory = null;
       IEchoService client = null;

       Console.WriteLine("Username authentication required.");
       Console.WriteLine(
         "Provide a valid machine or domain account. [domain\\user]");
       Console.WriteLine("   Enter username:");
       string username = Console.ReadLine();
       Console.WriteLine("   Enter password:");
       string password = "";
       ConsoleKeyInfo info = Console.ReadKey(true);
       while (info.Key != ConsoleKey.Enter)
       {
           if (info.Key != ConsoleKey.Backspace)
           {
               if (info.KeyChar != '\0')
               {
                   password += info.KeyChar;
                }
                info = Console.ReadKey(true);
            }
            else if (info.Key == ConsoleKey.Backspace)
            {
                if (password != "")
                {
                    password =
                       password.Substring(0, password.Length - 1);
                }
                info = Console.ReadKey(true);
            }
         }
         for (int i = 0; i < password.Length; i++)
            Console.Write("*");
         Console.WriteLine();
         try
         {
           // Create a proxy with the previously create binding and
           // endpoint address
              channelFactory =
                 new ChannelFactory<IEchoService>(
                     multipleTokensBinding, serviceAddress);
           // configure the username credentials, the client
           // certificate and the server certificate on the channel
           // factory
           channelFactory.Credentials.UserName.UserName = username;
           channelFactory.Credentials.UserName.Password = password;
           channelFactory.Credentials.ClientCertificate.SetCertificate(
           "CN=client.com", StoreLocation.CurrentUser, StoreName.My);
              channelFactory.Credentials.ServiceCertificate.SetDefaultCertificate(
           "CN=localhost", StoreLocation.LocalMachine, StoreName.My);
           client = channelFactory.CreateChannel();
           Console.WriteLine("Echo service returned: {0}",
                                           client.Echo());

           ((IChannel)client).Close();
           channelFactory.Close();
        }
        catch (CommunicationException e)
        {
         Abort((IChannel)client, channelFactory);
         // if there is a fault then print it out
         FaultException fe = null;
         Exception tmp = e;
         while (tmp != null)
         {
            fe = tmp as FaultException;
            if (fe != null)
            {
                break;
            }
            tmp = tmp.InnerException;
        }
        if (fe != null)
        {
           Console.WriteLine("The server sent back a fault: {0}",
         fe.CreateMessageFault().Reason.GetMatchingTranslation().Text);
        }
        else
        {
         Console.WriteLine("The request failed with exception: {0}",e);
        }
    }
    catch (TimeoutException)
    {
        Abort((IChannel)client, channelFactory);
        Console.WriteLine("The request timed out");
    }
    catch (Exception e)
    {
         Abort((IChannel)client, channelFactory);
          Console.WriteLine(
          "The request failed with unexpected exception: {0}", e);
    }
    Console.WriteLine();
    Console.WriteLine("Press <ENTER> to terminate client.");
    Console.ReadLine();
}

Exibindo as informações dos chamadores

Para exibir as informações do chamador, você pode usar ServiceSecurityContext.Current.AuthorizationContext.ClaimSets, conforme mostrado no código a seguir. ServiceSecurityContext.Current.AuthorizationContext.ClaimSets contém declarações de autorização associadas ao chamador atual. Essas declarações são fornecidas automaticamente pelo WCF (Windows Communication Foundation) para cada token recebido na mensagem.

bool TryGetClaimValue<TClaimResource>(ClaimSet claimSet, string
                         claimType, out TClaimResource resourceValue)
    where TClaimResource : class
{
    resourceValue = default(TClaimResource);
    IEnumerable<Claim> matchingClaims =
    claimSet.FindClaims(claimType, Rights.PossessProperty);
    if (matchingClaims == null)
          return false;
    IEnumerator<Claim> enumerator = matchingClaims.GetEnumerator();
    if (enumerator.MoveNext())
    {
        resourceValue = (enumerator.Current.Resource == null) ? null : (enumerator.Current.Resource as TClaimResource);
        return true;
    }
    else
    {
         return false;
    }
}

// Returns the username and certificate subject name provided by the client
void GetCallerIdentities(ServiceSecurityContext callerSecurityContext, out string userName, out string certificateSubjectName)
{
    userName = null;
    certificateSubjectName = null;

    // Look in all the claimsets in the authorization context
    foreach (ClaimSet claimSet in
      callerSecurityContext.AuthorizationContext.ClaimSets)
    {
        if (claimSet is WindowsClaimSet)
        {
            // Try to find a Name claim. This will have been generated
            //from the windows username.
            string tmpName;
            if (TryGetClaimValue<string>(claimSet, ClaimTypes.Name,
                                                     out tmpName))
            {
                userName = tmpName;
            }
        }
        else if (claimSet is X509CertificateClaimSet)
         {
            //Try to find an X500DistinguishedName claim.
            //This will have been generated from the client
            //certificate.
            X500DistinguishedName tmpDistinguishedName;
            if (TryGetClaimValue<X500DistinguishedName>(claimSet,
               ClaimTypes.X500DistinguishedName,
               out tmpDistinguishedName))
            {
                    certificateSubjectName = tmpDistinguishedName.Name;
            }
        }
    }
}

Executando o exemplo

Quando você executa o exemplo, o cliente primeiro solicita que você forneça o nome de usuário e a senha para o token de nome de usuário. Certifique-se de fornecer valores corretos para sua conta do sistema, pois o WCF no serviço mapeia os valores fornecidos no token de nome de usuário para a identidade fornecida pelo sistema. Depois disso, o cliente exibe a resposta do serviço. Pressione ENTER na janela do cliente para desligar o cliente.

Arquivo de configuração em lote

O arquivo em lote Setup.bat incluído com este exemplo permite que você configure o servidor com certificados relevantes para executar o aplicativo hospedado dos Serviços de Informações da Internet (IIS) que requer segurança baseada em certificado do servidor. Esse arquivo em lote precisa ser modificado para funcionar em computadores ou em um caso não auto-hospedado.

A seguir, oferecemos uma visão geral das diferentes seções dos arquivos em lote que podem ser modificadas para executar a configuração apropriada.

Criação do certificado do cliente

As linhas a seguir do arquivo em lote Setup.bat criam o certificado do cliente a ser usado. A variável %CLIENT_NAME% especifica o assunto do certificado do cliente. Este exemplo usa "client.com" como o nome da entidade.

O certificado é armazenado no repositório Meu (Pessoal) no local de armazenamento CurrentUser.

echo ************
echo making client cert
echo ************
makecert.exe -sr CurrentUser -ss MY -a sha1 -n CN=%CLIENT_NAME% -sky exchange -pe

Instalando o Certificado do cliente no Repositório confiável do servidor

A linha a seguir no arquivo em lote Setup.bat copia o certificado do cliente no repositório de pessoas confiáveis do servidor. Essa etapa é necessária porque os certificados gerados por Makecert.exe não são implicitamente confiáveis para o sistema do servidor. Se você já tiver um certificado com raiz em um certificado raiz confiável do cliente, por exemplo, um certificado emitido pela Microsoft, essa etapa de preenchimento do repositório de certificados do cliente com o certificado do servidor não será necessária.

echo ************
echo copying client cert to server's CurrentUserstore
echo ************
certmgr.exe -add -r CurrentUser -s My -c -n %CLIENT_NAME% -r LocalMachine -s TrustedPeople

Criação do certificado do servidor

As linhas a seguir do arquivo em lote Setup.bat criam o certificado do servidor a ser usado. A variável %SERVER_NAME% especifica o nome do servidor. Altere essa variável para especificar o nome do seu próprio servidor. O padrão neste arquivo de lote é localhost.

O certificado é armazenado no repositório Meu (Pessoal) no local de armazenamento LocalMachine. O certificado é armazenado no repositório LocalMachine para os serviços hospedados no IIS. Para serviços auto-hospedados, você deve modificar o arquivo em lote para armazenar o certificado do servidor no local do repositório CurrentUser substituindo a cadeia de caracteres LocalMachine por CurrentUser.

echo ************
echo Server cert setup starting
echo %SERVER_NAME%
echo ************
echo making server cert
echo ************
makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN=%SERVER_NAME% -sky exchange -pe

Instalação do Certificado do servidor no Repositório de certificados confiáveis do cliente.

As linhas a seguir no arquivo em lote Setup.bat copiam o certificado do servidor no repositório de pessoas confiáveis do cliente. Essa etapa é necessária porque os certificados gerados por Makecert.exe não são implicitamente confiáveis pelo sistema do cliente. Se você já tiver um certificado com raiz em um certificado raiz confiável do cliente, por exemplo, um certificado emitido pela Microsoft, essa etapa de preenchimento do repositório de certificados do cliente com o certificado do servidor não será necessária.

echo ************
echo copying server cert to client's TrustedPeople store
echo ************certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r CurrentUser -s TrustedPeople

Habilitando o acesso à chave privada do certificado

Para habilitar o acesso à chave privada do certificado do serviço hospedado no IIS, a conta de usuário na qual o processo hospedado no IIS está em execução deve receber as permissões apropriadas para a chave privada. Isso é feito nas últimas etapas no script Setup.bat.

echo ************
echo setting privileges on server certificates
echo ************
for /F "delims=" %%i in ('"%ProgramFiles%\ServiceModelSampleTools\FindPrivateKey.exe" My LocalMachine -n CN^=%SERVER_NAME% -a') do set PRIVATE_KEY_FILE=%%i
set WP_ACCOUNT=NT AUTHORITY\NETWORK SERVICE
(ver | findstr /C:"5.1") && set WP_ACCOUNT=%COMPUTERNAME%\ASPNET
echo Y|cacls.exe "%PRIVATE_KEY_FILE%" /E /G "%WP_ACCOUNT%":R
iisreset
Para configurar, compilar, e executar o exemplo
  1. Verifique se você executou as Amostras de Procedimento de Instalação Única para o Windows Communication Foundation.

  2. Para compilar a solução, siga as instruções contidas em Compilar as amostras do Windows Communication Foundation.

  3. Para executar a amostra em uma configuração de computador único ou entre computadores, use as instruções a seguir.

Para executar o exemplo no mesmo computador
  1. Execute Setup.bat na pasta de instalação de exemplo dentro de um prompt de comando do Visual Studio executado com privilégios de administrador. Isso instalará todos os certificados necessários para executar a amostra.

    Observação

    O arquivo de lote Setup.bat foi projetado para ser executado a partir de um prompt de comando do Visual Studio. A variável de ambiente PATH definida no prompt de comando do Visual Studio aponta para o diretório que contém executáveis exigidos pelo script Setup.bat. Lembre-se de remover os certificados executando Cleanup.bat quando terminar de usar o exemplo. Outros exemplos de segurança usam os mesmos certificados.

  2. Inicialize o Client.exe a partir do \client\bin. A atividade do cliente é exibida no aplicativo do console do cliente.

  3. Se o cliente e o serviço não puderem se comunicar, confira Dicas de solução de problemas para exemplos de WCF.

Para executar o exemplo entre computadores
  1. Criar um diretório no computador de serviço. Crie um aplicativo virtual chamado servicemodelsamples para esse diretório usando a ferramenta de gerenciamento dos Serviços de Informações da Internet (IIS).

  2. Copie os arquivos do programa de serviço de \inetpub\wwwroot\servicemodelsamples para o diretório virtual no computador de serviço. Certifique-se de copiar os arquivos no subdiretório \bin. Copie também os arquivos Setup.bat, Cleanup.bat e ImportClientCert.bat para o computador de serviço.

  3. Crie um diretório no computador cliente para os binários do cliente.

  4. Copie os arquivos do programa do cliente para o diretório do cliente no computador cliente. Copie também os arquivos Setup.bat, Cleanup.bat e ImportServiceCert.bat para o cliente.

  5. No servidor, execute setup.bat service em um Prompt de Comando do Desenvolvedor para Visual Studio aberto com privilégios de administrador. A execução de setup.bat com o argumento service cria um certificado de serviço com o nome de domínio totalmente qualificado do computador e exporta o certificado de serviço para um arquivo chamado Service.cer.

  6. Edite Web.config para que ele reflita o novo nome do certificado (no atributo findValue em <serviceCertificate>), que é o mesmo que o nome de domínio totalmente qualificado do computador.

  7. Copie o arquivo Service.cer do diretório de serviço para o diretório do cliente no computador cliente.

  8. No cliente, execute setup.bat client em um Prompt de Comando do Desenvolvedor para Visual Studio aberto com privilégios de administrador. A execução de setup.bat com o argumento client cria um certificado de cliente chamado client.com e exporta o certificado do cliente para um arquivo chamado Client.cer.

  9. No arquivo Client.exe.config do computador cliente, altere o valor do endereço do ponto de extremidade para que corresponda ao novo endereço do seu serviço. Faça isso substituindo localhost pelo nome de domínio totalmente qualificado do servidor.

  10. Copie o arquivo Client.cer do diretório do cliente para o diretório de serviço no servidor.

  11. No cliente, execute ImportServiceCert.bat. Isso importará o certificado de serviço do arquivo Service.cer para o repositório CurrentUser - TrustedPeople.

  12. No servidor, execute ImportClientCert.bat. Isso importará o certificado do cliente do arquivo Client.cer para o repositório LocalMachine – TrustedPeople.

  13. No computador cliente, inicialize Client.exe a partir de uma janela do prompt de comando. Se o cliente e o serviço não puderem se comunicar, confira Dicas de solução de problemas para amostras de WCF.

Para fazer uma limpeza após o exemplo
  • Execute Cleanup.bat na pasta de amostras depois de concluir a execução da amostra.

Observação

Esse script não remove os certificados de serviço em um cliente na execução dessa amostra em vários computadores. Se você tiver executado amostras do WCF (Windows Communication Foundation) que usam certificados em vários computadores, lembre-se de limpar os certificados de serviço instalados no repositório CurrentUser – TrustedPeople. Para fazer isso, use o seguinte comando: certmgr -del -r CurrentUser -s TrustedPeople -c -n <Fully Qualified Server Machine Name> Por exemplo: certmgr -del -r CurrentUser -s TrustedPeople -c -n server1.contoso.com.