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.
C#
publicstaticclassBindingHelper
{
publicstatic Binding CreateCreditCardBinding()
{
var 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.var messageSecurity = new SymmetricSecurityBindingElement();
messageSecurity.EndpointSupportingTokenParameters.SignedEncrypted.Add(new CreditCardTokenParameters());
X509SecurityTokenParameters x509ProtectionParameters = new X509SecurityTokenParameters();
x509ProtectionParameters.InclusionMode = SecurityTokenInclusionMode.Never;
messageSecurity.ProtectionTokenParameters = x509ProtectionParameters;
returnnew 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.
C#
classEchoServiceHost : ServiceHost
{
string creditCardFile;
publicEchoServiceHost(parameters Uri[] addresses)
: base(typeof(EchoService), addresses)
{
creditCardFile = ConfigurationManager.AppSettings["creditCardFile"];
if (string.IsNullOrEmpty(creditCardFile))
{
thrownew ConfigurationErrorsException("creditCardFile not specified in service config");
}
creditCardFile = String.Format("{0}\\{1}", System.Web.Hosting.HostingEnvironment.ApplicationPhysicalPath, creditCardFile);
}
overrideprotectedvoidInitializeRuntime()
{
// 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.
C#
Binding creditCardBinding = BindingHelper.CreateCreditCardBinding();
var serviceAddress = new EndpointAddress("http://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.var 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: {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.
C#
classCreditCardToken : SecurityToken
{
CreditCardInfo cardInfo;
DateTime effectiveTime = DateTime.UtcNow;
string id;
ReadOnlyCollection<SecurityKey> securityKeys;
publicCreditCardToken(CreditCardInfo cardInfo) : this(cardInfo, Guid.NewGuid().ToString()) { }
publicCreditCardToken(CreditCardInfo cardInfo, string id)
{
if (cardInfo == null)
thrownew ArgumentNullException(nameof(cardInfo));
if (id == null)
thrownew ArgumentNullException(nameof(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 { returnthis.cardInfo; } }
publicoverride ReadOnlyCollection<SecurityKey> SecurityKeys { get { returnthis.securityKeys; } }
publicoverride DateTime ValidFrom { get { returnthis.effectiveTime; } }
publicoverride DateTime ValidTo { get { returnthis.cardInfo.ExpirationDate; } }
publicoverridestring Id { get { returnthis.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.
C#
publicclassCreditCardSecurityTokenSerializer : WSSecurityTokenSerializer
{
publicCreditCardSecurityTokenSerializer(SecurityTokenVersion version) : base() { }
protectedoverrideboolCanReadTokenCore(XmlReader reader)
{
XmlDictionaryReader localReader = XmlDictionaryReader.CreateDictionaryReader(reader);
if (reader == null)
thrownew ArgumentNullException(nameof(reader));
if (reader.IsStartElement(Constants.CreditCardTokenName, Constants.CreditCardTokenNamespace))
returntrue;
returnbase.CanReadTokenCore(reader);
}
protectedoverride SecurityToken ReadTokenCore(XmlReader reader, SecurityTokenResolver tokenResolver)
{
if (reader == null)
thrownew ArgumentNullException(nameof(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();
var cardInfo = new CreditCardInfo(creditCardNumber, creditCardIssuer, expirationTime);
returnnew CreditCardToken(cardInfo, id);
}
else
{
return WSSecurityTokenSerializer.DefaultInstance.ReadToken(reader, tokenResolver);
}
}
protectedoverrideboolCanWriteTokenCore(SecurityToken token)
{
if (token is CreditCardToken)
returntrue;
returnbase.CanWriteTokenCore(token);
}
protectedoverridevoidWriteTokenCore(XmlWriter writer, SecurityToken token)
{
if (writer == null)
thrownew ArgumentNullException(nameof(writer));
if (token == null)
thrownew ArgumentNullException(nameof(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 CreditCardClientCredentials, CreditCardClientCredentialsSecurityTokenManager and CreditCardTokenProvider classes.
On the service, the functionality resides in the CreditCardServiceCredentials, CreditCardServiceCredentialsSecurityTokenManager, CreditCardTokenAuthenticator and CreditCardTokenAuthorizationPolicy classes.
C#
publicclassCreditCardClientCredentials : ClientCredentials
{
CreditCardInfo creditCardInfo;
publicCreditCardClientCredentials(CreditCardInfo creditCardInfo)
: base()
{
if (creditCardInfo == null)
thrownew ArgumentNullException(nameof(creditCardInfo));
this.creditCardInfo = creditCardInfo;
}
public CreditCardInfo CreditCardInfo
{
get { returnthis.creditCardInfo; }
}
protectedoverride ClientCredentials CloneCore()
{
returnnew CreditCardClientCredentials(this.creditCardInfo);
}
publicoverride SecurityTokenManager CreateSecurityTokenManager()
{
returnnew CreditCardClientCredentialsSecurityTokenManager(this);
}
}
publicclassCreditCardClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager
{
CreditCardClientCredentials creditCardClientCredentials;
publicCreditCardClientCredentialsSecurityTokenManager(CreditCardClientCredentials creditCardClientCredentials)
: base (creditCardClientCredentials)
{
this.creditCardClientCredentials = creditCardClientCredentials;
}
publicoverride SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement)
{
// Handle this token for Custom.if (tokenRequirement.TokenType == Constants.CreditCardTokenType)
returnnew CreditCardTokenProvider(this.creditCardClientCredentials.CreditCardInfo);
// Return server cert.elseif (tokenRequirement is InitiatorServiceModelSecurityTokenRequirement)
{
if (tokenRequirement.TokenType == SecurityTokenTypes.X509Certificate)
{
returnnew X509SecurityTokenProvider(creditCardClientCredentials.ServiceCertificate.DefaultCertificate);
}
}
returnbase.CreateSecurityTokenProvider(tokenRequirement);
}
publicoverride SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version)
{
returnnew CreditCardSecurityTokenSerializer(version);
}
}
classCreditCardTokenProvider : SecurityTokenProvider
{
CreditCardInfo creditCardInfo;
publicCreditCardTokenProvider(CreditCardInfo creditCardInfo) : base()
{
if (creditCardInfo == null)
thrownew ArgumentNullException(nameof(creditCardInfo));
this.creditCardInfo = creditCardInfo;
}
protectedoverride SecurityToken GetTokenCore(TimeSpan timeout)
{
SecurityToken result = new CreditCardToken(this.creditCardInfo);
return result;
}
}
publicclassCreditCardServiceCredentials : ServiceCredentials
{
string creditCardFile;
publicCreditCardServiceCredentials(string creditCardFile)
: base()
{
if (creditCardFile == null)
thrownew ArgumentNullException(nameof(creditCardFile));
this.creditCardFile = creditCardFile;
}
publicstring CreditCardDataFile
{
get { returnthis.creditCardFile; }
}
protectedoverride ServiceCredentials CloneCore()
{
returnnew CreditCardServiceCredentials(this.creditCardFile);
}
publicoverride SecurityTokenManager CreateSecurityTokenManager()
{
returnnew CreditCardServiceCredentialsSecurityTokenManager(this);
}
}
publicclassCreditCardServiceCredentialsSecurityTokenManager : ServiceCredentialsSecurityTokenManager
{
CreditCardServiceCredentials creditCardServiceCredentials;
publicCreditCardServiceCredentialsSecurityTokenManager(CreditCardServiceCredentials creditCardServiceCredentials)
: base(creditCardServiceCredentials)
{
this.creditCardServiceCredentials = creditCardServiceCredentials;
}
publicoverride SecurityTokenAuthenticator CreateSecurityTokenAuthenticator(SecurityTokenRequirement tokenRequirement, out SecurityTokenResolver outOfBandTokenResolver)
{
if (tokenRequirement.TokenType == Constants.CreditCardTokenType)
{
outOfBandTokenResolver = null;
returnnew CreditCardTokenAuthenticator(creditCardServiceCredentials.CreditCardDataFile);
}
returnbase.CreateSecurityTokenAuthenticator(tokenRequirement, out outOfBandTokenResolver);
}
publicoverride SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version)
{
returnnew CreditCardSecurityTokenSerializer(version);
}
}
classCreditCardTokenAuthenticator : SecurityTokenAuthenticator
{
string creditCardsFile;
publicCreditCardTokenAuthenticator(string creditCardsFile)
{
this.creditCardsFile = creditCardsFile;
}
protectedoverrideboolCanValidateTokenCore(SecurityToken token)
{
return (token is CreditCardToken);
}
protectedoverride ReadOnlyCollection<IAuthorizationPolicy> ValidateTokenCore(SecurityToken token)
{
CreditCardToken creditCardToken = token as CreditCardToken;
if (creditCardToken.CardInfo.ExpirationDate < DateTime.UtcNow)
thrownew SecurityTokenValidationException("The credit card has expired.");
if (!IsCardNumberAndExpirationValid(creditCardToken.CardInfo))
thrownew 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 issuervar cardIssuerClaimSet = new DefaultClaimSet(new Claim(ClaimTypes.Name, creditCardToken.CardInfo.CardIssuer, Rights.PossessProperty));
var cardClaimSet = new DefaultClaimSet(cardIssuerClaimSet, new Claim(Constants.CreditCardNumberClaim, creditCardToken.CardInfo.CardNumber, Rights.PossessProperty));
var 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>privateboolIsCardNumberAndExpirationValid(CreditCardInfo cardInfo)
{
try
{
using (var 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
{
returnfalse;
}
}
}
returnfalse;
}
}
catch (Exception e)
{
thrownew Exception("BookStoreService: Error while retrieving credit card information from User DB " + e.ToString());
}
}
}
publicclassCreditCardTokenAuthorizationPolicy : IAuthorizationPolicy
{
string id;
ClaimSet issuer;
IEnumerable<ClaimSet> issuedClaimSets;
publicCreditCardTokenAuthorizationPolicy(ClaimSet issuedClaims)
{
if (issuedClaims == null)
thrownew ArgumentNullException(nameof(issuedClaims));
this.issuer = issuedClaims.Issuer;
this.issuedClaimSets = new ClaimSet[] { issuedClaims };
this.id = Guid.NewGuid().ToString();
}
public ClaimSet Issuer { get { returnthis.issuer; } }
publicstring Id { get { returnthis.id; } }
publicboolEvaluate(EvaluationContext context, refobject state)
{
foreach (ClaimSet issuance inthis.issuedClaimSets)
{
context.AddClaimSet(this, issuance);
}
returntrue;
}
}
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.
C#
boolTryGetStringClaimValue(ClaimSet claimSet, string claimType, outstring claimValue)
{
claimValue = null;
IEnumerable<Claim> matchingClaims = claimSet.FindClaims(claimType, Rights.PossessProperty);
if (matchingClaims == null)
returnfalse;
IEnumerator<Claim> enumerator = matchingClaims.GetEnumerator();
enumerator.MoveNext();
claimValue = (enumerator.Current.Resource == null) ? null :
enumerator.Current.Resource.ToString();
returntrue;
}
stringGetCallerCreditCardNumber()
{
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$"Credit card '{creditCardNumber}' issued by '{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 computers 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.
bat
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.
bat
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.
bat
echo ************
echo setting privileges on server certificates
echo ************
for /F "delims=" %%iin ('"%ProgramFiles%\ServiceModelSampleTools\FindPrivateKey.exe" My LocalMachine -n CN^=%SERVER_NAME% -a') doset PRIVATE_KEY_FILE=%%iset 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 Visual Studio Command Prompt. The PATH environment variable set within the Visual Studio Command Prompt points to the directory that contains executables required by the Setup.bat script.
Open a Visual Studio Command Prompt window with administrator privileges and run Setup.bat from the sample install folder. This installs all the certificates required for running the sample.Make sure that the path includes the folder where Makecert.exe is located.
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.
Create a directory on the service computer for the service binaries.
Copy the service program files into the service directory on the service computer. 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 computer.
You must have a server certificate with the subject name that contains the fully-qualified domain name of the computer. You can create one using the Setup.bat if you change the %SERVER_NAME% variable to fully-qualified name of the computer where the service is hosted. Note that the Setup.bat file must be run in a Developer Command Prompt for Visual Studio opened with administrator privileges.
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 computer name instead of localhost.
Copy the client program files from the \client\bin\ folder, under the language-specific folder, to the client computer.
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 computer name of the remote host instead of localhost.
On the client computer, launch Client.exe from a command prompt window.
Microservice applications, because of their distributed nature, can be difficult to secure. In this module, you'll learn how to classify sensitive data in a cloud-native application, redact sensitive data in log files, and generate compliance reports for a cloud-native application.