Supporting Tokens
Cet exemple montre comment ajouter des jetons supplémentaires à un message qui utilise WS-Security. L'exemple ajoute un jeton de sécurité binaire X.509 outre un jeton de sécurité de nom d'utilisateur. Le jeton est passé dans un en-tête de message WS-Security du client au service et une partie du message est signée avec la clé privée associée au jeton de sécurité X.509 pour prouver la possession du certificat X.509 au récepteur. Cela s’avère utile dans le cas où plusieurs revendications doivent être associées à un message pour authentifier ou autoriser l’expéditeur. Le service implémente un contrat qui définit un modèle de communication demande-réponse.
Illustre le
L'exemple montre :
Comment un client peut passer des jetons de sécurité supplémentaires à un service.
Comment le serveur peut accéder aux revendications associées aux jetons de sécurité supplémentaires.
Comment le certificat X.509 du serveur permet de protéger la clé symétrique utilisée pour la signature et le chiffrement des messages.
Notes
La procédure d'installation ainsi que les instructions de génération relatives à cet exemple figurent à la fin de cette rubrique.
Le client s'authentifie à l'aide du jeton de nom d'utilisateur et du jeton de sécurité X.509 de prise en charge
Le service expose un point de terminaison unique de communication qui est créé par programme à l'aide des classes BindingHelper
et EchoServiceHost
. Le point de terminaison se compose d'une adresse, d'une liaison et d'un contrat. La liaison est configurée avec une liaison personnalisé à l'aide de SymmetricSecurityBindingElement
et HttpTransportBindingElement
. Cet exemple oblige SymmetricSecurityBindingElement
à utiliser un certificat X.509 du service pour protéger la clé symétrique pendant la transmission et à passer un UserNameToken
avec le X509SecurityToken
de prise en charge dans un en-tête de message WS-Security. La clé symétrique permet de chiffrer le corps du message et le jeton de sécurité de nom d'utilisateur. Le jeton de prise en charge est passé comme jeton de sécurité binaire supplémentaire dans l'en-tête de message WS-Security. L'authenticité du jeton de prise en charge est prouvée en signant une partie du message avec la clé privée associée au jeton de sécurité X.509 de prise en charge.
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);
}
Le comportement spécifie les informations d'identification du service qui doivent être utilisées pour l'authentification du client ainsi que les informations sur le certificat X.509 du service. L'exemple utilise CN=localhost
comme nom du sujet dans le certificat X.509 du service.
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();
}
Code de service :
[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;
}
}
}
}
}
Le point de terminaison client est configuré de la même manière que le point de terminaison de service. Le client utilise une classe BindingHelper
identique pour créer sa liaison. La suite de la configuration s'effectue dans classe Client
. Le client définit les formations sur le jeton de sécurité de nom d’utilisateur, le jeton de sécurité X.509 de prise en charge et les informations sur le certificat X.509 du service dans le code d’installation vers la collection de comportements de point de terminaison client.
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();
}
Affichage des informations sur les appelants
Pour afficher les informations sur l'appelant, vous pouvez utiliser ServiceSecurityContext.Current.AuthorizationContext.ClaimSets
, tel qu'indiqué dans le code suivant. ServiceSecurityContext.Current.AuthorizationContext.ClaimSets
contient les revendications d'autorisation associées à l'appelant actuel. Ces revendications sont automatiquement fournies par Windows Communication Foundation (WCF) pour chaque jeton reçu dans le message.
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;
}
}
}
}
Exécution de l'exemple
Lorsque vous exécutez l'exemple, le client vous invite d'abord à fournir un nom d'utilisateur et un mot de passe pour le jeton de nom d'utilisateur. Veillez à fournir des valeurs correctes pour votre compte système, car WCF sur le service mappe les valeurs fournies dans le jeton de nom d’utilisateur sur l’identité fournie par le système. Ceci fait, le client affiche la réponse provenant du service. Appuyez sur Entrée dans la fenêtre du client pour l'arrêter.
Fichier de commandes d'installation
Le fichier de commandes Setup.bat inclus avec cet exemple vous permet de configurer le serveur avec les certificats appropriés pour exécuter l'application hébergée IIS (Internet Information Services) qui requiert une sécurité basée sur le certificat du serveur. Ce fichier de commandes doit être modifié pour fonctionner sur plusieurs ordinateurs ou sans hébergement.
Les éléments suivants fournissent une vue d'ensemble des différentes sections des fichiers de commandes afin qu'ils puissent être modifiés pour s'exécuter dans la configuration appropriée.
Création du certificat client
Les lignes suivantes du fichier de commandes Setup.bat créent le certificat client à utiliser. La variable %CLIENT_NAME%
spécifie le sujet du certificat client. Cet exemple utilise "client.com" comme nom du sujet.
Le certificat est stocké dans le magasin My (personnel) sous l'emplacement de magasin CurrentUser
.
echo ************
echo making client cert
echo ************
makecert.exe -sr CurrentUser -ss MY -a sha1 -n CN=%CLIENT_NAME% -sky exchange -pe
Installation du certificat client dans le magasin approuvé du serveur :
La ligne suivante du fichier de commandes Setup.bat copie le certificat client dans le magasin de personnes de confiance du serveur. Cette étape est nécessaire car les certificats générés par Makecert.exe ne sont pas implicitement approuvés par le système du serveur. Si vous disposez déjà d'un certificat associé à un certificat racine approuvé du client, par exemple un certificat émis par Microsoft, cette étape de remplissage du magasin de certificats client avec le certificat de serveur n'est pas requise.
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
Création du certificat de serveur
Les lignes suivantes du fichier de commandes Setup.bat créent le certificat de serveur à utiliser. La variable %SERVER_NAME%
spécifie le nom du serveur. Modifiez cette variable pour spécifier votre propre nom de serveur. La valeur par défaut dans ce fichier de commandes est localhost.
Le certificat est stocké dans le magasin My (personnel) sous l'emplacement de magasins LocalMachine. Le certificat est stocké dans le magasin LocalMachine pour les services hébergés par IIS. Pour les services auto-hébergés, vous devez modifier le fichier de commandes afin de stocker le certificat de serveur dans l'emplacement de magasin CurrentUser en remplaçant la chaîne LocalMachine par 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
Installation du certificat de serveur dans le magasin de certificats approuvé du client :
Les lignes suivantes du fichier de commandes Setup.bat copient le certificat de serveur dans le magasin de personnes de confiance du client. Cette étape est requise car les certificats générés par Makecert.exe ne sont pas implicitement approuvés par le système client. Si vous disposez déjà d'un certificat associé à un certificat racine approuvé du client, par exemple un certificat émis par Microsoft, cette étape de remplissage du magasin de certificats client avec le certificat de serveur n'est pas requise.
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
Activation de l'accès à la clé privée du certificat
Pour activer l'accès à la clé privée du certificat à partir du service hébergé par IIS, le compte d'utilisateur sous lequel le processus hébergé par IIS s'exécute doit disposer des autorisations permettant d'y accéder. Cette opération est effectuée par les dernières étapes du 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
Pour configurer, générer et exécuter l'exemple
Assurez-vous d’avoir effectué la Procédure d’installation unique pour les exemples Windows Communication Foundation.
Pour générer la solution, suivez les instructions indiquées dans la rubrique Génération des exemples Windows Communication Foundation.
Pour exécuter l'exemple dans une configuration à un ou plusieurs ordinateurs, utilisez les instructions suivantes.
Pour exécuter l'exemple sur le même ordinateur
Ouvrez une invite de commandes Visual Studio avec des privilèges d’administrateur et exécutez Setup.bat à partir du dossier d’installation de l’exemple. Tous les certificats requis à l'exécution de l'exemple sont ainsi installés.
Notes
Le fichier de commandes Setup.bat est conçu pour s'exécuter à partir d'une invite de commandes de Visual Studio. La variable d’environnement PATH définie dans l’invite de commandes Visual Studio pointe vers le répertoire qui contient les exécutables requis par le script Setup.bat. Assurez-vous de supprimer les certificats en exécutant Cleanup.bat une fois l'exemple terminé. D'autres exemples de sécurité utilisent ces mêmes certificats.
Lancez Client.exe à partir de \client\bin. L'activité du client s'affiche sur son application de console.
Si le client et le service ne parviennent pas à communiquer, consultez Conseils de dépannage pour les exemples WCF.
Pour exécuter l'exemple sur plusieurs ordinateurs
Créez un répertoire sur l'ordinateur de service. Créez une application virtuelle appelée servicemodelsamples pour ce répertoire à l'aide de l'outil de gestion IIS (Internet Information Services).
Copiez les fichiers du programme de service figurant dans le dossier \inetpub\wwwroot\servicemodelsamples dans ce répertoire virtuel. Assurez-vous de copier les fichiers dans le sous-répertoire \bin. Copiez également les fichiers Setup.bat, Cleanup.bat et ImportClientCert.bat sur l'ordinateur de service.
Créez un répertoire sur l'ordinateur client pour les fichiers binaires du client.
Copiez les fichiers programme du client dans le répertoire client de l'ordinateur client. Copiez également les fichiers Setup.bat, Cleanup.bat et ImportServiceCert.bat sur le client.
Sur le serveur, ouvrez une Invite de commandes développeur pour Visual Studio avec des privilèges d’administrateur et exécutez
setup.bat service
. L’exécution desetup.bat
à l’aide de l’argumentservice
crée un certificat de service portant le nom de domaine complet de l’ordinateur, puis exporte ce certificat vers un fichier appelé Service.cer.Modifiez Web.config pour refléter le nouveau nom de certificat (dans l’attribut
findValue
de <serviceCertificate>) qui est identique au nom de domaine complet de l’ordinateur.Copiez le fichier Service.cer du répertoire de service dans le répertoire client sur l'ordinateur client.
Sur le client, ouvrez une Invite de commandes développeur pour Visual Studio avec des privilèges d’administrateur et exécutez
setup.bat client
. L'exécution desetup.bat
à l'aide de l'argumentclient
crée un certificat client appelé client.com, puis exporte ce certificat vers un fichier nommé Client.cer.Dans le fichier Client.exe.config sur l'ordinateur client, modifiez la valeur d'adresse du point de terminaison pour qu'il corresponde à la nouvelle adresse de votre service. Pour ce faire, remplacez localhost par le nom de domaine complet du serveur.
Copiez le fichier Client.cer du répertoire client dans le répertoire de service sur le serveur.
Sur le client, exécutez ImportServiceCert.bat. Cette opération importe le certificat de service du fichier Service.cer dans le magasin CurrentUser - TrustedPeople.
Sur le serveur, exécutez ImportClientCert.bat. Cette opération importe le certificat client du fichier Client.cer dans le magasin LocalMachine - TrustedPeople.
Sur l'ordinateur du client, lancez Client.exe à partir d'une fenêtre d'invite de commandes. Si le client et le service ne parviennent pas à communiquer, consultez Conseils de dépannage pour les exemples WCF.
Pour procéder au nettoyage après exécution de l'exemple
- Exécutez Cleanup.bat dans le dossier d'exemples après avoir exécuté l'exemple.
Notes
Ce script ne supprime pas les certificats de service figurant sur le client lorsque l'exemple est exécuté sur plusieurs ordinateurs. Si vous avez exécuté des exemples WCF qui utilisent des certificats sur plusieurs ordinateurs, veillez à effacer les certificats de service installés dans le magasin CurrentUser - TrustedPeople. Pour ce faire, utilisez la commande suivante : certmgr -del -r CurrentUser -s TrustedPeople -c -n <Fully Qualified Server Machine Name>
, par exemple : certmgr -del -r CurrentUser -s TrustedPeople -c -n server1.contoso.com
.