Custom Token
This sample demonstrates how to add a custom token implementation into a Windows Communication Foundation (WCF) application. The example uses a CreditCardToken
to securely pass information about client credit cards to the service. The token is passed in the WS-Security message header and is signed and encrypted using the symmetric security binding element along with message body and other message headers. This is useful in cases where the built-in tokens are not sufficient. This sample demonstrates how to provide a custom security token to a service instead of using one of the built-in tokens. The service implements a contract that defines a request-reply communication pattern.
Note: |
---|
The setup procedure and build instructions for this sample are located at the end of this topic. |
To summarize, this sample demonstrates the following:
How a client can pass a custom security token to a service.
How the service can consume and validate a custom security token.
How the WCF service code can obtain the information about received security tokens including the custom security token.
How the server's X.509 certificate is used to protect the symmetric key used for message encryption and signature.
Client Authentication Using a Custom Security Token
The service exposes a single endpoint that is programmatically created using BindingHelper
and EchoServiceHost
classes. The endpoint consists of an address, a binding, and a contract. The binding is configured with a custom binding using SymmetricSecurityBindingElement
and HttpTransportBindingElement
. This sample sets the SymmetricSecurityBindingElement
to use a service's X.509 certificate to protect the symmetric key during transmission and to pass a custom CreditCardToken
in a WS-Security message header as a signed and encrypted security token. The behavior specifies the service credentials that are to be used for client authentication and also information about the service X.509 certificate.
public static class BindingHelper
{
public static Binding CreateCreditCardBinding()
{
HttpTransportBindingElement httpTransport = new HttpTransportBindingElement();
// The message security binding element will be configured to require a credit card.
// The token that is encrypted with the service's certificate.
SymmetricSecurityBindingElement messageSecurity = new SymmetricSecurityBindingElement();
messageSecurity.EndpointSupportingTokenParameters.SignedEncrypted.Add(new CreditCardTokenParameters());
X509SecurityTokenParameters x509ProtectionParameters = new X509SecurityTokenParameters();
x509ProtectionParameters.InclusionMode = SecurityTokenInclusionMode.Never;
messageSecurity.ProtectionTokenParameters = x509ProtectionParameters;
return new CustomBinding(messageSecurity, httpTransport);
}
}
To consume a credit card token in the message, the sample uses custom service credentials to provide this functionality. The service credentials class is located in the CreditCardServiceCredentials
class and is added to the behaviors collections of the service host in the EchoServiceHost.InitializeRuntime
method.
class EchoServiceHost : ServiceHost
{
string creditCardFile;
public EchoServiceHost(params Uri[] addresses)
: base(typeof(EchoService), addresses)
{
creditCardFile = ConfigurationManager.AppSettings["creditCardFile"];
if (string.IsNullOrEmpty(creditCardFile))
{
throw new ConfigurationErrorsException("creditCardFile not specified in service config");
}
creditCardFile = String.Format("{0}\\{1}", System.Web.Hosting.HostingEnvironment.ApplicationPhysicalPath, creditCardFile);
}
override protected void InitializeRuntime()
{
// Create a credit card service credentials and add it to the behaviors.
CreditCardServiceCredentials serviceCredentials = new CreditCardServiceCredentials(this.creditCardFile);
serviceCredentials.ServiceCertificate.SetCertificate("CN=localhost", StoreLocation.LocalMachine, StoreName.My);
this.Description.Behaviors.Remove((typeof(ServiceCredentials)));
this.Description.Behaviors.Add(serviceCredentials);
// Register a credit card binding for the endpoint.
Binding creditCardBinding = BindingHelper.CreateCreditCardBinding();
this.AddServiceEndpoint(typeof(IEchoService), creditCardBinding, string.Empty);
base.InitializeRuntime();
}
}
The client endpoint is configured in a similar manner as the service endpoint. The client uses the same BindingHelper
class to create a binding. The rest of the setup is located in the Client
class. The client also sets information to be contained in the CreditCardToken
and information about the service X.509 certificate in the setup code by adding a CreditCardClientCredentials
instance with the proper data to the client endpoint behaviors collection. The sample uses X.509 certificate with subject name set to CN=localhost
as the service certificate.
Binding creditCardBinding = BindingHelper.CreateCreditCardBinding();
EndpointAddress serviceAddress = new EndpointAddress("https://localhost/servicemodelsamples/service.svc");
// Create a client with given client endpoint configuration
channelFactory =
new ChannelFactory<IEchoService>(creditCardBinding, serviceAddress);
// configure the credit card credentials on the channel factory
CreditCardClientCredentials credentials =
new CreditCardClientCredentials(
new CreditCardInfo(creditCardNumber, issuer, expirationTime));
// configure the service certificate on the credentials
credentials.ServiceCertificate.SetDefaultCertificate(
"CN=localhost", StoreLocation.LocalMachine, StoreName.My);
// replace ClientCredentials with CreditCardClientCredentials
channelFactory.Endpoint.Behaviors.Remove(typeof(ClientCredentials));
channelFactory.Endpoint.Behaviors.Add(credentials);
client = channelFactory.CreateChannel();
Console.WriteLine("Echo service returned: {0}", client.Echo());
((IChannel)client).Close();
channelFactory.Close();
Custom Security Token Implementation
To enable a custom security token in WCF, create an object representation of the custom security token. The sample has this representation in the CreditCardToken
class. The object representation is responsible for holding all relevant security token information and to provide a list of security keys contained in the security token. In this case, the credit card security token does not contain any security key.
The next section describes what must be done to enable a custom token to be transmitted over the wire and consumed by a WCF endpoint.
class CreditCardToken : SecurityToken
{
CreditCardInfo cardInfo;
DateTime effectiveTime = DateTime.UtcNow;
string id;
ReadOnlyCollection<SecurityKey> securityKeys;
public CreditCardToken(CreditCardInfo cardInfo) : this(cardInfo, Guid.NewGuid().ToString()) { }
public CreditCardToken(CreditCardInfo cardInfo, string id)
{
if (cardInfo == null)
throw new ArgumentNullException("cardInfo");
if (id == null)
throw new ArgumentNullException("id");
this.cardInfo = cardInfo;
this.id = id;
// The credit card token is not capable of any cryptography.
this.securityKeys = new ReadOnlyCollection<SecurityKey>(new List<SecurityKey>());
}
public CreditCardInfo CardInfo { get { return this.cardInfo; } }
public override ReadOnlyCollection<SecurityKey> SecurityKeys { get { return this.securityKeys; } }
public override DateTime ValidFrom { get { return this.effectiveTime; } }
public override DateTime ValidTo { get { return this.cardInfo.ExpirationDate; } }
public override string Id { get { return this.id; } }
}
Getting the Custom Credit Card Token to and from the Message
Security token serializers in WCF are responsible for creating an object representation of security tokens from the XML in the message and creating a XML form of the security tokens. They are also responsible for other functionality such as reading and writing key identifiers pointing to security tokens, but this example uses only security token-related functionality. To enable a custom token you must implement your own security token serializer. This sample uses the CreditCardSecurityTokenSerializer
class for this purpose.
On the service, the custom serializer reads the XML form of the custom token and creates the custom token object representation from it.
On the client, the CreditCardSecurityTokenSerializer
class writes the information contained in the security token object representation into the XML writer.
public class CreditCardSecurityTokenSerializer : WSSecurityTokenSerializer
{
public CreditCardSecurityTokenSerializer(SecurityTokenVersion version) : base() { }
protected override bool CanReadTokenCore(XmlReader reader)
{
XmlDictionaryReader localReader = XmlDictionaryReader.CreateDictionaryReader(reader);
if (reader == null) throw new ArgumentNullException("reader");
if (reader.IsStartElement(Constants.CreditCardTokenName, Constants.CreditCardTokenNamespace))
return true;
return base.CanReadTokenCore(reader);
}
protected override SecurityToken ReadTokenCore(XmlReader reader, SecurityTokenResolver tokenResolver)
{
if (reader == null) throw new ArgumentNullException("reader");
if (reader.IsStartElement(Constants.CreditCardTokenName, Constants.CreditCardTokenNamespace))
{
string id = reader.GetAttribute(Constants.Id, Constants.WsUtilityNamespace);
reader.ReadStartElement();
// Read the credit card number.
string creditCardNumber = reader.ReadElementString(Constants.CreditCardNumberElementName, Constants.CreditCardTokenNamespace);
// Read the expiration date.
string expirationTimeString = reader.ReadElementString(Constants.CreditCardExpirationElementName, Constants.CreditCardTokenNamespace);
DateTime expirationTime = XmlConvert.ToDateTime(expirationTimeString, XmlDateTimeSerializationMode.Utc);
// Read the issuer of the credit card.
string creditCardIssuer = reader.ReadElementString(Constants.CreditCardIssuerElementName, Constants.CreditCardTokenNamespace);
reader.ReadEndElement();
CreditCardInfo cardInfo = new CreditCardInfo(creditCardNumber, creditCardIssuer, expirationTime);
return new CreditCardToken(cardInfo, id);
}
else
{
return WSSecurityTokenSerializer.DefaultInstance.ReadToken(reader, tokenResolver);
}
}
protected override bool CanWriteTokenCore(SecurityToken token)
{
if (token is CreditCardToken)
return true;
else
return base.CanWriteTokenCore(token);
}
protected override void WriteTokenCore(XmlWriter writer, SecurityToken token)
{
if (writer == null) { throw new ArgumentNullException("writer"); }
if (token == null) { throw new ArgumentNullException("token"); }
CreditCardToken c = token as CreditCardToken;
if (c != null)
{
writer.WriteStartElement(Constants.CreditCardTokenPrefix, Constants.CreditCardTokenName, Constants.CreditCardTokenNamespace);
writer.WriteAttributeString(Constants.WsUtilityPrefix, Constants.Id, Constants.WsUtilityNamespace, token.Id);
writer.WriteElementString(Constants.CreditCardNumberElementName, Constants.CreditCardTokenNamespace, c.CardInfo.CardNumber);
writer.WriteElementString(Constants.CreditCardExpirationElementName, Constants.CreditCardTokenNamespace, XmlConvert.ToString(c.CardInfo.ExpirationDate, XmlDateTimeSerializationMode.Utc));
writer.WriteElementString(Constants.CreditCardIssuerElementName, Constants.CreditCardTokenNamespace, c.CardInfo.CardIssuer);
writer.WriteEndElement();
writer.Flush();
}
else
{
base.WriteTokenCore(writer, token);
}
}
}
How Token Provider and Token Authenticator Classes are Created
Client and service credentials are responsible for providing the security token manager instance. The security token manager instance is used to get token providers, token authenticators and token serializers.
The token provider creates an object representation of the token based on the information contained in the client or service credentials. The token object representation is then written to the message using the token serializer (discussed in the previous section).
The token authenticator validates tokens that arrive in the message. The incoming token object representation is created by the token serializer. This object representation is then passed to the token authenticator for validation. After the token is successfully validated, the token authenticator returns a collection of IAuthorizationPolicy
objects that represent the information contained in the token. This information is used later during the message processing to perform authorization decisions and to provide claims for the application. In this example, the credit card token authenticator uses CreditCardTokenAuthorizationPolicy
for this purpose.
The token serializer is responsible for getting the object representation of the token to and from the wire. This is discussed in the previous section.
In this sample, we use a token provider only on the client and a token authenticator only on the service, because we want to transmit a credit card token only in the client-to-service direction.
The functionality on the client is located in the CreditCardClientCrendentials
, CreditCardClientCredentialsSecurityTokenManager
and CreditCardTokenProvider
classes.
On the service, the functionality resides in the CreditCardServiceCredentials
, CreditCardServiceCredentialsSecurityTokenManager
, CreditCardTokenAuthenticator
and CreditCardTokenAuthorizationPolicy
classes.
public class CreditCardClientCredentials : ClientCredentials
{
CreditCardInfo creditCardInfo;
public CreditCardClientCredentials(CreditCardInfo creditCardInfo)
: base()
{
if (creditCardInfo == null)
throw new ArgumentNullException("creditCardInfo");
this.creditCardInfo = creditCardInfo;
}
public CreditCardInfo CreditCardInfo
{
get { return this.creditCardInfo; }
}
protected override ClientCredentials CloneCore()
{
return new CreditCardClientCredentials(this.creditCardInfo);
}
public override SecurityTokenManager CreateSecurityTokenManager()
{
return new CreditCardClientCredentialsSecurityTokenManager(this);
}
}
public class CreditCardClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager
{
CreditCardClientCredentials creditCardClientCredentials;
public CreditCardClientCredentialsSecurityTokenManager(CreditCardClientCredentials creditCardClientCredentials)
: base (creditCardClientCredentials)
{
this.creditCardClientCredentials = creditCardClientCredentials;
}
public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement)
{
// handle this token for Custom
if (tokenRequirement.TokenType == Constants.CreditCardTokenType)
return new CreditCardTokenProvider(this.creditCardClientCredentials.CreditCardInfo);
// return server cert
else if (tokenRequirement is InitiatorServiceModelSecurityTokenRequirement)
{
if (tokenRequirement.TokenType == SecurityTokenTypes.X509Certificate)
{
return new X509SecurityTokenProvider(creditCardClientCredentials.ServiceCertificate.DefaultCertificate);
}
}
return base.CreateSecurityTokenProvider(tokenRequirement);
}
public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version)
{
return new CreditCardSecurityTokenSerializer(version);
}
}
class CreditCardTokenProvider : SecurityTokenProvider
{
CreditCardInfo creditCardInfo;
public CreditCardTokenProvider(CreditCardInfo creditCardInfo) : base()
{
if (creditCardInfo == null)
{
throw new ArgumentNullException("creditCardInfo");
}
this.creditCardInfo = creditCardInfo;
}
protected override SecurityToken GetTokenCore(TimeSpan timeout)
{
SecurityToken result = new CreditCardToken(this.creditCardInfo);
return result;
}
}
public class CreditCardServiceCredentials : ServiceCredentials
{
string creditCardFile;
public CreditCardServiceCredentials(string creditCardFile)
: base()
{
if (creditCardFile == null)
throw new ArgumentNullException("creditCardFile");
this.creditCardFile = creditCardFile;
}
public string CreditCardDataFile
{
get { return this.creditCardFile; }
}
protected override ServiceCredentials CloneCore()
{
return new CreditCardServiceCredentials(this.creditCardFile);
}
public override SecurityTokenManager CreateSecurityTokenManager()
{
return new CreditCardServiceCredentialsSecurityTokenManager(this);
}
}
public class CreditCardServiceCredentialsSecurityTokenManager : ServiceCredentialsSecurityTokenManager
{
CreditCardServiceCredentials creditCardServiceCredentials;
public CreditCardServiceCredentialsSecurityTokenManager(CreditCardServiceCredentials creditCardServiceCredentials)
: base(creditCardServiceCredentials)
{
this.creditCardServiceCredentials = creditCardServiceCredentials;
}
public override SecurityTokenAuthenticator CreateSecurityTokenAuthenticator(SecurityTokenRequirement tokenRequirement, out SecurityTokenResolver outOfBandTokenResolver)
{
if (tokenRequirement.TokenType == Constants.CreditCardTokenType)
{
outOfBandTokenResolver = null;
return new CreditCardTokenAuthenticator(creditCardServiceCredentials.CreditCardDataFile);
}
return base.CreateSecurityTokenAuthenticator(tokenRequirement, out outOfBandTokenResolver);
}
public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version)
{
return new CreditCardSecurityTokenSerializer(version);
}
}
class CreditCardTokenAuthenticator : SecurityTokenAuthenticator
{
string creditCardsFile;
public CreditCardTokenAuthenticator(string creditCardsFile)
{
this.creditCardsFile = creditCardsFile;
}
protected override bool CanValidateTokenCore(SecurityToken token)
{
return (token is CreditCardToken);
}
protected override ReadOnlyCollection<IAuthorizationPolicy> ValidateTokenCore(SecurityToken token)
{
CreditCardToken creditCardToken = token as CreditCardToken;
if (creditCardToken.CardInfo.ExpirationDate < DateTime.UtcNow)
throw new SecurityTokenValidationException("The credit card has expired");
if (!IsCardNumberAndExpirationValid(creditCardToken.CardInfo))
throw new SecurityTokenValidationException("Unknown or invalid credit card");
// the credit card token has only 1 claim - the card number. The issuer for the claim is the
// credit card issuer
DefaultClaimSet cardIssuerClaimSet = new DefaultClaimSet(new Claim(ClaimTypes.Name, creditCardToken.CardInfo.CardIssuer, Rights.PossessProperty));
DefaultClaimSet cardClaimSet = new DefaultClaimSet(cardIssuerClaimSet, new Claim(Constants.CreditCardNumberClaim, creditCardToken.CardInfo.CardNumber, Rights.PossessProperty));
List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>(1);
policies.Add(new CreditCardTokenAuthorizationPolicy(cardClaimSet));
return policies.AsReadOnly();
}
/// <summary>
/// Helper method to check if a given credit card entry is present in the User DB
/// </summary>
private bool IsCardNumberAndExpirationValid(CreditCardInfo cardInfo)
{
try
{
using (StreamReader myStreamReader = new StreamReader(this.creditCardsFile))
{
string line = "";
while ((line = myStreamReader.ReadLine()) != null)
{
string[] splitEntry = line.Split('#');
if (splitEntry[0] == cardInfo.CardNumber)
{
string expirationDateString = splitEntry[1].Trim();
DateTime expirationDateOnFile = DateTime.Parse(expirationDateString, System.Globalization.DateTimeFormatInfo.InvariantInfo, System.Globalization.DateTimeStyles.AdjustToUniversal);
if (cardInfo.ExpirationDate == expirationDateOnFile)
{
string issuer = splitEntry[2];
return issuer.Equals(cardInfo.CardIssuer, StringComparison.InvariantCultureIgnoreCase);
}
else
{
return false;
}
}
}
return false;
}
}
catch (Exception e)
{
throw new Exception("BookStoreService: Error while retrieving credit card information from User DB " + e.ToString());
}
}
}
public class CreditCardTokenAuthorizationPolicy : IAuthorizationPolicy
{
string id;
ClaimSet issuer;
IEnumerable<ClaimSet> issuedClaimSets;
public CreditCardTokenAuthorizationPolicy(ClaimSet issuedClaims)
{
if (issuedClaims == null)
throw new ArgumentNullException("issuedClaims");
this.issuer = issuedClaims.Issuer;
this.issuedClaimSets = new ClaimSet[] { issuedClaims };
this.id = Guid.NewGuid().ToString();
}
public ClaimSet Issuer { get { return this.issuer; } }
public string Id { get { return this.id; } }
public bool Evaluate(EvaluationContext context, ref object state)
{
foreach (ClaimSet issuance in this.issuedClaimSets)
{
context.AddClaimSet(this, issuance);
}
return true;
}
}
Displaying the Callers' Information
To display the caller's information, use the ServiceSecurityContext.Current.AuthorizationContext.ClaimSets
as shown in the following sample code. The ServiceSecurityContext.Current.AuthorizationContext.ClaimSets
contains authorization claims associated with the current caller. The claims are supplied by the CreditCardToken
class in its AuthorizationPolicies
collection.
bool TryGetStringClaimValue(ClaimSet claimSet, string claimType, out string claimValue)
{
claimValue = null;
IEnumerable<Claim> matchingClaims = claimSet.FindClaims(claimType,
Rights.PossessProperty);
if (matchingClaims == null)
return false;
IEnumerator<Claim> enumerator = matchingClaims.GetEnumerator();
enumerator.MoveNext();
claimValue = (enumerator.Current.Resource == null) ? null :
enumerator.Current.Resource.ToString();
return true;
}
string GetCallerCreditCardNumber()
{
foreach (ClaimSet claimSet in
ServiceSecurityContext.Current.AuthorizationContext.ClaimSets)
{
string creditCardNumber = null;
if (TryGetStringClaimValue(claimSet,
Constants.CreditCardNumberClaim, out creditCardNumber))
{
string issuer;
if (!TryGetStringClaimValue(claimSet.Issuer,
ClaimTypes.Name, out issuer))
{
issuer = "Unknown";
}
return String.Format(
"Credit card '{0}' issued by '{1}'",
creditCardNumber, issuer);
}
}
return "Credit card is not known";
}
When you run the sample, the operation requests and responses are displayed in the client console window. Press ENTER in the client window to shut down the client.
Setup Batch File
The Setup.bat batch file included with this sample allows you to configure the server with relevant certificates to run the IIS-hosted application that requires server certificate-based security. This batch file must be modified to work across machines or to work in a non-hosted case.
The following provides a brief overview of the different sections of the batch files so that they can be modified to run in the appropriate configuration.
Creating the server certificate:
The following lines from the Setup.bat batch file create the server certificate to be used. The
%SERVER_NAME%
variable specifies the server name. Change this variable to specify your own server name. The default in this batch file is localhost. If you change the%SERVER_NAME%
variable, you must go through the Client.cs and Service.cs files and replace all instances of localhost with the server name that you use in the Setup.bat script.The certificate is stored in My (Personal) store under the LocalMachine store location. The certificate is stored in the LocalMachine store for the IIS-hosted services. For self-hosted services, you should modify the batch file to store the client certificate in the CurrentUser store location by replacing the string LocalMachine with 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
Installing the server certificate into client's trusted certificate store:
The following lines in the Setup.bat batch file copy the server certificate into the client trusted people store. This step is required because certificates generated by Makecert.exe are not implicitly trusted by the client system. If you already have a certificate that is rooted in a client trusted root certificate—for example, a Microsoft issued certificate—this step of populating the client certificate store with the server certificate is not required.
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
To enable access to the certificate private key from the IIS-hosted service, the user account under which the IIS-hosted process is running must be granted appropriate permissions for the private key. This is accomplished by last steps in the Setup.bat script.
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
Note: |
---|
The Setup.bat batch file is designed to be run from a Windows SDK Command Prompt. It requires that the MSSDK environment variable point to the directory where the SDK is installed. This environment variable is automatically set within a Windows SDK Command Prompt. |
To set up and build the sample
Ensure that you have performed the One-Time Setup Procedure for the Windows Communication Foundation Samples.
To build the solution, follow the instructions in Building the Windows Communication Foundation Samples.
To run the sample on the same machine
Make sure that the path includes the folder where Makecert.exe is located.
Run Setup.bat from the sample installation folder. This installs all the certificates required for running the sample.
Note: |
---|
Be sure to remove the certificates by running Cleanup.bat when finished with the sample. Other security samples use the same certificates. |
Launch Client.exe from client\bin directory. Client activity is displayed on the client console application.
If the client and service are not able to communicate, see Troubleshooting Tips.
To run the sample across machines
Create a directory on the service machine for the service binaries.
Copy the service program files into the service directory on the service machine. Do not forget to copy CreditCardFile.txt; otherwise the credit card authenticator cannot validate credit card information sent from the client. Also copy the Setup.bat and Cleanup.bat files to the service machine.
You must have a server certificate with the subject name that contains the fully-qualified domain name of the machine. You can create one using the Setup.bat if you change the
%SERVER_NAME%
variable to fully-qualified name of the machine where the service is hosted.Copy the server certificate into the CurrentUser-TrustedPeople store on the client. You must do this only if the server certificate is not issued by a trusted issuer.
In the EchoServiceHost.cs file, change the value of the certificate subject name to specify a fully-qualified machine name instead of localhost.
Copy the client program files from the \client\bin\ folder, under the language-specific folder, to the client machine.
In the Client.cs file, change the address value of the endpoint to match the new address of your service.
In the Client.cs file change the subject name of the service X.509 certificate to match the fully-qualified machine name of the remote host instead of localhost.
On the client machine, launch Client.exe from a command prompt window.
If the client and service are not able to communicate, see Troubleshooting Tips.
To clean up after the sample
- Run Cleanup.bat in the samples folder once you have finished running the sample.
Send comments about this topic to Microsoft.
© Microsoft Corporation. All rights reserved.