About
This project presents a Visual Studio solution including a simple demo WCF Authentication Service Application and a βTesterβ Client (Windows Form Application) that allows the user to test the user registration, login, logout, and service operations. In addition to demonstrating standard authentication capabilities, the WCF service implements a custom username and password validator pattern. Passwords are stored securely using Password-Based Key Derivation Function PBKD cryptology of which the implementation is discussed. A custom error handler ensures that exceptions are properly wrapped into WCF Faults and communicated to the client caller. Certificates are discussed along with how to implement a server certificate on a client machine for development testing of βintegrityβ and application trust. The project includes a demo certificate and script for generating self-signed dev certificates, which must be installed into the client certificate store for the client tester application to trust and access the demo service.Β The client βtesterβ windows form application is not intended as a UX/UI demo but used to test and verify that the backend authentication service registration, login, logout, service operations, and callbacks are working as expected and sending proper WCF fault messages. Lastly, the project is shown in the demo section with a video and screen captures.
Architecture
The demo project consists of these simple component topics:
- SharedLibrary – Class library shared amongst server and client describing service contract interfaces and common fault models.
- ServiceLibrary β Class library that implements the service contracts and backend features
- ServiceHost β Console Application that launches and hosts the Service
- ClientTester – Client βTester to Serviceβ Windows Form Application Project
SharedLibrary Service Contract Interfaces & Common Models
A Class Library project (SharedLibrary) was added to my Visual Studio solution. This library is shared amongst the client and the service. The code is available on GitHub [here].
IHelloAuthenticateService (Interface for Service)
The ServiceContract for the Authentication service contains the following operation contracts. The code is available on GitHub [here].
- bool Register (string username, string password)
- Register the user with the service. Only distinct usernames are allowed, and the client will receive a DuplicateUserFault if the username is already existing on the service. Note: credentials are passed using WCF security.
- bool Login (string username)
- Logins the user after it passes the custom validation (precursor) and also registers a callback to this user client to receive the service messages. Note: credentials are passed using WCF security.
- bool LogOff (string username)
- Logs off the user/client from the service and unregisters it to receive callbacks. Note: credentials are passed using WCF security.
- string GreetMe (string username)
- Receive a customized greeting by the service (if registered and logged in) or a default one.
Service Error Handling Interface Design
Each of the interfaces are labeled with the attributes of [FaultContract(typeof(GenericFault))] so that any exception caught on the service can be packaged as a fault object and sent to the caller with the detailed information regarding that exception. Furthermore, with the Register and Login operational contracts, another fault contract [FaultContract(typeof(DuplicateUserFault))] is specified so that it can catch and return issues with duplicate user exceptions in a detailed fault model instead of the generic one.
DuplicateUserFault (Class for Error/Exception/Fault Object)
The DuplicateUserFault class represents a specific error condition in the application that is an Exception (error) on the service and communicated as a fault in WCF to the client. It describes the specific error condition of a client trying to login with a username that is already registered or logged in to the service. This class had only one property, the Reason for the fault. The property is completed by the service application when it packages its exceptions as faults to send to the client caller. The fault is communicated to the client to handle and understand that it could not log in and why. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/// <summary> /// Duplicate User Fault /// Specific fault model for describing exceptions /// when a user tries to register or log in with an existing /// username. /// </summary> [DataContract] public class DuplicateUserFault { #region Properties [DataMember] public string Reason { get; set; } #endregion Properties } // end of class |
GenericFault (Class for Error/Exception/Fault Object)
The GenericFault class represents a general fault (exception) object in the application. Specifically, it is meant to model any exception that was caught and handled on the service when the client calls an operational contract. This class had only one property, the Reason for the fault. The property is completed by the service application when it packages its exceptions as faults to send to the client caller. The fault is communicated to the client to handle and understand that it could not log in and why. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/// <summary> /// GenericFault /// General Fault object model /// for all exceptions on service /// for client to understand. /// </summary> [DataContract] public class GenericFault { [DataMember] public string Reason { get; set; } } // end of class |
IServiceCallback (Interface for Service Callback)
The ServiceContract for the Authentication service contains only one callback to the client. Whenever a client is connected to the service, it will receive callbacks from the service with select informational messages happening at that instant in time (example: login and logoff). The client is required to implement the service callback interface in order to receive the instant messages. In addition, the operation behavior is set to one-way so that the service does not wait on a response from the client, which could cause performance issues. Therefore, this is a one-way message feature. The code is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/// <summary> /// Client Contract /// Client must implement this contract in order /// to particpate in duplex communications and /// receive messages from the service for normal /// (non-fault) information (ex: chat messages). /// </summary> [ServiceContract] public interface IServiceCallback { /// <summary> /// SendClientMessage /// Sends the Client recent messages /// (When it is connected) /// The service does not want to wait on a response from the client /// so there is no secondary callback to the service. This is oneway /// to the client only. /// </summary> /// <param name="message">(string) a message from the service</param> [OperationContract(IsOneWay = true)] void SendClientMessage(string message); } // end of interface |
ServiceLibrary Service Implementation and Features
The ServiceLibrary project implements and manages the service to client consumers. In addition to the main HelloAuthenticateService service class, it contains separate class design and organizational architecture to model users, cryptology requirements for secure password storage, custom username and password validation, and error handling. The subsections will discuss the ServiceLibrary project features. Β The complete project code is available on GitHub [here].
User Model
In this service application, a user is modeled with the User class.Β The class code is available on GitHub [here].
Fields
- String UserName – Username of the Registered User
- IServiceCallback Callback – Callback to Client User for Duplex Communications
- byte[] Salt – unique cryptologic salt for user
- byte[] Password – derived key password (after PBKD)
- int WorkFactor – number of iterations for the PBKD function
Constructor
The constructor creates a user using PBDK by first generating a salt, then performing work to derive a secure hash from the userβs password string converted Unicode byte array. PBDK cryptology and the implementation is discussed more in detail in the next section, but the User class utilizes the static methods in the ServiceCryptology class in this project to obtain PBDK. The PBDK is stored in the User model for that is made when a user registers on the service for the first time. Β Whenever the user attempts to login or logoff, their credentials are converted to PBDK and compared to the in-memory PBDK. First the User is matched/found on UserName and PBDK utilizes the existing/registered User salt, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/// <summary> /// Constructor /// </summary> /// <param name="username">(string) username for the user</param> /// <param name="password">(string) password for the user</param> /// <param name="callback">(IServiceCallback) for the user</param> public User(string username, string password, IServiceCallback callback) { UserName = username; Callback = callback; #region Use PBDK to Securely Create and Store Password // Generate a salt // The US National Institute of Standards and Technology // recommends a salt length of 128 bits. Salt = ServiceCryptology.GenerateSalt(128); // Convert String Password to Unicode Byte Array byte[] bytesPassword = Encoding.Unicode.GetBytes(password); // Create a Derived Key Password // Use default workfactor Password = ServiceCryptology.GenerateHash(bytesPassword, Salt, WorkFactor, 256); #endregion Use PBDK to Securely Create and Store Password } // end of method |
PBDK (Password-Based Key Derivation) Secure Password Cryptology
The ServiceCryptology class implements Password-Based Key Derivation Function PBKD for password management. PBKD is a key derivation function with a sliding computational cost, used to reduce vulnerabilities to brute force attacks. PBKDF2 applies a pseudorandom function, such as hash-based message authentication code (HMAC), to the input password or passphrase along with a salt value and repeats the process many times to produce a derived key, which can then be used as a cryptographic key in subsequent operations. The added computational work makes password cracking much more difficult, and is known as key stretching. [reference here]
Building my PBDK I referenced a blog article [here] that explains the steps of using PBDK in securing a user password on the back-end, etc.
The class code is available on GitHub [here].
Generating a Salt
In cryptography, a salt is random data that is used as an additional input to a one-way function that hashes data, a password or passphrase. Salts are used to safeguard passwords in storage. A new salt is randomly generated for each password. In a typical setting, the salt and the password (or its version after key stretching) are concatenated and processed with a cryptographic hash function, and the resulting output (but not the original password) is stored with the salt in a database. Hashing allows for later authentication without keeping and therefore risking exposure of the plaintext password in the event that the authentication data store is compromised. Salts defend against a pre-computed hash attack. Since salts do not have to be memorized by humans, they can make the size of the hash table required for a successful attack prohibitively large without placing a burden on the users. Since salts are different in each case, they also protect commonly used passwords, or those users who use the same password on several sites, by making all salted hash instances for the same password different from each other. [Reference here]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/// <param name="length">Length of the desired cryptographic salt</param> /// <returns>(byte[]) salt is a sequence of bits, known as a cryptographic salt</returns> public static byte[] GenerateSalt(int length) { var bytes = new byte[length]; using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) { rng.GetBytes(bytes); } return bytes; } // end of method |
Generating a Hash
This method computes and returns the generated derived password key.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/// <summary> /// GenerateHash /// Returns the generated derived password key /// </summary> /// <param name="password">(byte[]) Unicode encoded password</param> /// <param name="salt">(byte[]) salt is a sequence of bits, known as a cryptographic salt</param> /// <param name="iterations">(int) number of iterations</param> /// <param name="length">(int) the desired bit-length of the derived key</param> /// <returns>(byte[]) generated derived key</returns> public static byte[] GenerateHash(byte[] password, byte[] salt, int iterations, int length) { using (Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(password, salt, iterations)) { return deriveBytes.GetBytes(length); } } // end of method |
Custom Username and Password Validator
The UserNamePasswordAuthenticator class implements a “Custom User Name and Password Validator” pattern. By default, when a user name and password is used for authentication, Windows Communication Foundation (WCF) uses Windows to validate the user name and password. However, WCF allows for custom user name and password authentication schemes, also known as validators. The application configuration file on the Service Host (discussed in the next section) must include a configuration to point to the project library and class containing the custom validator:
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode=”Custom” customUserNamePasswordValidatorType=”ServiceLibrary.UserNamePasswordAuthenticator, ServiceLibrary”/>
The class code is available on GitHub [here]. Microsoft has documentation that further explains the custom validator and implementation [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/// <param name="username">(string) User credential "username"</param> /// <param name="password">(string) User credential "password"</param> public override void Validate(string username, string password) { #region ValidateUserRegistration // First try to find a registered user User user = HelloAuthenticateService.RegisteredUsers.FirstOrDefault(x => x.UserName == username); // Allow the Service Registration to Handle Unregistered User if (user == null) { // Do not validate non-existing users return; } #endregion ValidateUserRegistration #region AuthenticateRegisteredUser // Convert Password to Compare to Database Password // Password-Based Key Derivation Function byte[] bytesPassword = Encoding.Unicode.GetBytes(password); // Note: Cryptology values may not be suitable for production level, this is only a demo byte[] passwordHash = ServiceCryptology.GenerateHash(bytesPassword, user.Salt, user.WorkFactor, 256); // Authenticate the Existing User Password if (user != null && System.Text.Encoding.Default.GetString(user.Password) == System.Text.Encoding.Default.GetString(passwordHash)) { // Authenticated, no other action necessary } else { throw new SecurityTokenException("Unknown Username or Incorrect Password"); } #endregion AuthenticateRegisteredUser } // end of method |
Service Error Handler (Code that Handles Exceptions as Faults)
The service-wide error class ServiceErrorHandler handles all exceptions that are thrown from the service. The HelloAuthenticateService class inherits from this class and throws exceptions directly so that this class so that it can handle the exceptions. Those exceptions will be sent to clients in the Detail property of the enclosing FaultException<T> object. On the client side, if the FaultContract attribute (example: [FaultContract(typeof(GenericFault))]) is specified in the interface definition, a FaultException for that exception can be differentiated from others in try-catch statements. The ServiceErrorHandler class implements two required interfaces: IErrorHandler and IServiceBehavior that make the service-wide error handling possible.
If you are unfamiliar with a Service Error Handler, I created a demo project and blog article [link] that explains the design and implementation of communicating exceptions on a service back to the client with WCF faults.
The class code implementation of the Error Handler is available on GitHub [here].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/// <summary> /// Enables the creation of a custom FaultException<GenericFault> /// that is returned from an exception in the course of a service method. /// Important! Communications have to be duplex for the client to receive the fault! /// </summary> /// <param name="ex">The Exception object thrown in the course of the service operation.</param> /// <param name="version">The SOAP version of the message.</param> /// <param name="msg">The Message object that is returned to the client, or service, in the duplex case.</param> public void ProvideFault(Exception ex, MessageVersion version, ref Message msg) { // If the Exception was already caught as a FaultException type object, // we do not need to create a new one if (!(ex is FaultException)) { FaultException<GenericFault> fex = new FaultException<GenericFault>( new GenericFault() { Reason = ex.Message }, ex.Message ); // Communications have to be duplex for the client to receive the faults MessageFault fault = fex.CreateMessageFault(); msg = Message.CreateMessage(version, fault, fex.Action); } } // end of method |
Hello Authenticate Service
The HelloAuthenticateService class implements and manages the registration, login, logoff, client callback messaging, and greeting services to clients. It is a single instance “Shared Service” model for all client consumers and implements the IHelloAuthenticateService interface operational contracts that the client consumers expect from the SharedLibrary interface modeling. The class code is available on GitHub [here].
Properties
Two properties hold lists of registered and logged in users (User model). Since this is a demo, the service simulates all service information in-memory that would otherwise be retrieved from a backend datastore.
1 2 3 4 5 |
// List of Registered Users public static List<User> RegisteredUsers { get; private set; } // List of LoggedIn Users public static List<User> LoggedInUsers { get; private set; } |
Constructor
This is a simple constructor that initializes the list upon the service loading a single instance.
1 2 3 4 5 |
public HelloAuthenticateService() { RegisteredUsers = new List<User>(); LoggedInUsers = new List<User>(); } |
IHelloAuthenticateService Interface Implementation
The service must implement the IHelloAuthenticateService interface operational contracts described and referenced in the SharedLibrary project.Β They are described in detail below.
Register
The client sends a request to register themselves with a username and password. First the custom validator is called before this method. If they are already registered, the custom validator tries to validate their credentials. If unregistered, the validator allows the program to flow to this method without any error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/// <summary> /// Register /// The client sends a request to register themselves /// with a username and password. If they are already registered /// the CustomValidation tries to validate their credentials /// </summary> /// <param name="username">(string) user name</param> /// <param name="password">(string) password</param> /// <returns>(boolean) if successful, false otherwise</returns> public bool Register(string username, string password) { // Registration Success Flag bool registered = false; // Obtain callback for the client IServiceCallback callback = OperationContext.Current.GetCallbackChannel<IServiceCallback>(); // First Verify User Does Not Already Exist in Registration if (!(RegisteredUsers.Any(x => x.UserName == username))) { // User is Not Registered, Register User // Create new user User user = new User(username, password, callback); /// Add to Registered Users RegisteredUsers.Add(user); // Set Return Value registered = true; Console.WriteLine($"User {username} registered..."); } else { DuplicateUserFault fault = new DuplicateUserFault() { Reason = "User '" + username + "' already registered." }; throw new FaultException<DuplicateUserFault>(fault, fault.Reason); } return registered; } // end of Register method |
Login
This logs in a registered user and throws any exceptions as faults to the user (custom Error Handler). First the custom validator is called before this method. If they are already registered the custom validator tries to validate their credentials. If unsuccessful, the exception is sent to the user immediately and this Login method is never called.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
/// <summary> /// Login /// Logs in a registered user. /// Throws faults (to client) if there are issues /// </summary> /// <param name="username">(string) username to login</param> /// <returns>(boolean) true if successful, false otherwise</returns> public bool Login(string username) { bool loggedin = false; // First Verify User Not Already Logged In if (!(LoggedInUsers.Any(x => x.UserName == username))) { // See if User is Registered if (RegisteredUsers.Any(x => x.UserName == username)) { // User is Already Authenticated Per Custom Validator LoggedInUsers.Add(RegisteredUsers.Find(x => x.UserName == username)); loggedin = true; Console.WriteLine($"User {username} logged in."); // Send message to callback SendMessageToUsers($"User {username} logged in."); } // User is Not Registered else { loggedin = false; // This will be sent to client as a generic fault throw new ArgumentException("User: " + username + " is not registered"); } } else { loggedin = false; // Send custom fault to client DuplicateUserFault fault = new DuplicateUserFault() { Reason = "User '" + username + "' already logged in!" }; throw new FaultException<DuplicateUserFault>(fault, fault.Reason); } return loggedin; } // end of method |
LogOff
This log out a registered user and throws any exceptions as faults to the user (custom Error Handler). First the custom validator is called before this method. If they are already registered the custom validator tries to validate their credentials. If unsuccessful, the exception is sent to the user immediately and this LogOff method is never called.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/// <summary> /// LoggOff /// Logs off an existing registered and logged in user /// Throws exception (then handled as a generic fault to client) if /// the user is not logged in. /// </summary> /// <param name="username">(string) username to log off</param> /// <returns>(boolean) if logoff is successful, false otherwise</returns> public bool LogOff(string username) { bool loggedoff = false; // Find the user by username (distinct) User user = LoggedInUsers?.Find(x => x.UserName == username); if (user != null) { // User already validated LoggedInUsers.Remove(user); loggedoff = true; Console.WriteLine($"User {username} logged out..."); SendMessageToUsers($"User {username} logged out."); } // throws fault to user else { throw new ArgumentException("User: " + username + " is not logged in."); } return loggedoff; } // end of method |
GreetMe
The GreetMe method returns a custom greeting to an authenticated user or a default general message to a stranger. First the custom validator is called before this method. If the user is unregistered, then the custom validator will allow the program to flow to this method. If they are already registered the custom validator tries to validate their credentials. If unsuccessful, the exception is sent to the user immediately and this GreetMe method is never called.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/// <summary> /// GreetMe /// Returns a custom greeting to an authenticated user /// or a default general message to a stranger /// </summary> /// <param name="username">(string) user name</param> /// <returns>(string) custom greeting to a user, or default response</returns> public string GreetMe(string username) { // Find the user by username (distinct) User user = LoggedInUsers?.Find(x => x.UserName == username); if (user != null) { // User already validated return $"Hello, {username}. You are successfully registered and logged in."; } else { return "Stranger: Please first register, then login to get your custom user greeting."; } } // end of Greet Me |
Callback Methods
SendMessageToUsers
The SendMessageToUsers method sends a message to all currently logged in users using the IServiceCallback callback property of each registered and logged in user (User model) to invoke the interface method “SendClientMessage(message)” on the client to receive and process the message. If there is an exception thrown (ex: the client is disconnected), then it automatically logs out the user from the service. The section βClient Testerβ in this article discusses the callback service contract implementation on the client end.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/// <summary> /// SendMessageToUsers /// Sends a message to all logged in users /// </summary> /// <param name="message">(string) message to user</param> private void SendMessageToUsers(string message) { // Sens message for each logged in user foreach (User user in LoggedInUsers) { try { IServiceCallback callback = user.Callback; callback.SendClientMessage(message); } // Catches an issue with a user disconnect and logs off that user catch (Exception) { // Remove the disconnected client LogOff(user.UserName); } } // end of foreach } // end of method |
ServiceHost Hosting the Service
The ServiceHost project is a simple console application responsible for starting, managing, and stopping the Authentication service. This demo hosts the service using dual binding netTcpBinding protocol for duplex communications so the caller can receive faults and callbacks on a custom port address as described in the application configuration file.Β The code is available on GitHub [here].
I did first experiment with and utilize the wsDualHttpBinding binding protocol but noticed it did not properly communicate faults or exceptions in WCF message security that were thrown in the custom validation class (first step) of a client call to the service. The protocol binding did work for normal duplex communications, but had some unresolved issue in not being able to throw any exception back to the client during the secure message validation phase. I would recommend first try using the netTcpBinding protocol and if needed, then creating and implementing an additional error handling interface so it can be shared in both classes. Due to time/effort constraints on my demo project, I decided not to pursue that implementation to try and get it to work 100% with the WS protocol.
Main Program
The main program is the entry point for the service host application. It creates a ServiceHost object of type described in the ServiceLibrary, opens the host up for connections, and waits for the user to manually close the service (pressing enter in the console window).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/// <summary> /// Main /// Main entry for program /// </summary> /// <param name="args">none used</param> static void Main(string[] args) { try { Console.WriteLine("Starting Hello Authenticate Demo Service..."); // Note: Do not put this service host constructor within a using clause. // Errors in Open will be trumped by errors from Close (implicitly called from ServiceHost.Dispose). System.ServiceModel.ServiceHost host = new System.ServiceModel.ServiceHost(typeof(HelloAuthenticateService)); host.Open(); Console.WriteLine("The Hello Authenticate Demo Service has started."); Console.WriteLine("Press <ENTER> to quit."); Console.ReadLine(); host.Close(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name); Console.WriteLine("An error occurred: " + ex.Message); Console.WriteLine("Press <ENTER> to quit."); Console.ReadLine(); } } // end of main method |
Application Configuration App.Config
The application configuration simple sets up the service model configuration including the endpoint, interface contract name, protocol, and the base address for this demo. The service host uses this information to correctly host the service. The code is available on GitHub [here].
The application configuration specifies additional characteristics of the communication:
- Service Behaviors
- Fault Exception Details
- Extension β Error Handling
- Service Credentials
- Client Certificate Requirements
- Service Certificate Details (discussed in next section)
- Custom User Authentication Instructions
- Service Endpoint
- Name
- Binding
- Configuration Names
- Contract
- Base Address
- Binding
- Security Specification βMessageβ
- Buffer/Quota Sizes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<?xml version="1.0"?> <configuration> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name="ServerBehavior"> <serviceMetadata/> <serviceDebug includeExceptionDetailInFaults="true"/> <serviceCredentials> <clientCertificate> <authentication certificateValidationMode="None" revocationMode="NoCheck"/> </clientCertificate> <serviceCertificate findValue="WCF Demo Server" storeLocation="LocalMachine" storeName="Root" x509FindType="FindBySubjectName"/> <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="ServiceLibrary.UserNamePasswordAuthenticator, ServiceLibrary"/> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> <extensions> <behaviorExtensions> <add name="ErrorHandler" type="HelloAuthenticateService.Error"/> </behaviorExtensions> </extensions> <services> <service name="ServiceLibrary.HelloAuthenticateService" behaviorConfiguration="ServerBehavior"> <endpoint address="" binding="netTcpBinding" bindingConfiguration="myTcp" contract="SharedLibrary.IHelloAuthenticateService"/> <host> <baseAddresses> <add baseAddress="net.tcp://localhost:31001/Hello"/> </baseAddresses> </host> </service> </services> <bindings> <netTcpBinding> <binding name="myTcp" maxBufferPoolSize="60000000" maxReceivedMessageSize="60000000"> <security mode="Message"> <message clientCredentialType="UserName"/> </security> <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/> </binding> </netTcpBinding> </bindings> </system.serviceModel> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/> </startup> </configuration> |
Certificates
This project uses and demos a certificate to establish trust from the client connecting to the service. In production scenarios, the service implementation would require the purchase a trusted certificate that inherits from a root third-party trusted certificate provider (example: VeriSign), of which the client already has installed on their computer certificate store. In development scenarios such as this, certificates are self-generated and installed on a server and/or client machine for testing the service. The client can also embed certificates in their application configuration file for trustworthy WCF. The certificate and generation code for this project is available on GitHub [here].
Good to Know
In Windows platform, the below file types are used for certificate information such as the SSL and Public Key Infrastructure (X.509) certificate types.
CER
The CER file is used to store the X.509 certificate, which is used for SSL certification to verify and identify an Internet server trust and security. The CER file contains information about certificate owner and public key. A CER file can be in binary or encoded with Base-64. The certificate generation script (discussed below) exports the certificate into binary format. However, after it is installed on the server, it needs to be converted/exported into base-64 encoded format using a wizard from the certificate store.
PVK
Stands for Private Key. Windows uses PVK files to store private keys for code signing in various Microsoft products. PVK is proprietary format.
PFX
PFX, Personal Exchange Format, is a PKCS12 file. This contains a variety of cryptographic information, such as certificates, root authority certificates, certificate chains and private keys. Itβs cryptographically protected with passwords to keep private keys private and preserve the integrity of the root certificates. The PFX file is also used in various Microsoft products, such as IIS. This project created a PFX file in the certificate generation process.
Certificate Generation
The simple script file CertificateGenerator.bat creates a certificate called βWCF Demo Serverβ along with additional files. The script file is available on GitHub [here]
1 2 3 4 |
REM Passwords: password makecert -r -pe -n "CN=WCF Demo Server" -b 01/01/2020 -e 01/01/2045 -sky exchange WCFDemoServer.cer -sv WCFDemoServer.pvk pvk2pfx.exe -pvk WCFDemoServer.pvk -spc WCFDemoServer.cer -pfx WCFDemoServer.pfx -po password |
Installation
The specific certificate needs to be installed into the server certificate store in order for this project demo to successful work, otherwise, the certificate configuration can be commented out for untrusted client -> server communications. The blog article [here] is a good reference on how to install a custom development certificate into the serverβs certificate store for testing the project.
Conversion to Base-64 Encoding
In order to setup the client application configuration model of WCF with certificate trust to an Internet server, the option is to either 1.) manually put the server certificate in the local user certificate store and reference this certificate in the client application configuration file or 2.) to embed the certificate directly using the base64 encoding string. This project embeds the certificate information directly into the client application configuration file for WCF (see Client Tester section). Since this project does not use SVCUTIL to build a proxy, but instead uses the ChannelFactory classes, the base64 encoding string must be obtained from the certificate. The blog article [here] discusses how to obtain a Base-64 Encoding of your certificate for insertion into the clientβs application configuration file.
Application Configurations
Service
The Service Host uses an application configuration file [link here] to specify its trusted certificate. Note: This particular demo and project implementation does not require the client be trusted by the service (no client certificate required) and is open to serving anyone.
1 2 3 4 5 6 7 8 9 |
<serviceCredentials> <clientCertificate> <authentication certificateValidationMode="None" revocationMode="NoCheck"/> </clientCertificate> <serviceCertificate findValue="WCF Demo Server" storeLocation="LocalMachine" storeName="Root" x509FindType="FindBySubjectName"/> <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="ServiceLibrary.UserNamePasswordAuthenticator, ServiceLibrary"/> </serviceCredentials> |
Client
The ClientTester project uses an application configuration [GitHub link here] file that embeds a base64 encoded string of the certificate that it will trust for the service configuration. WCF communications verify this is the same certificate stored on the server as specified in the service application configuration file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<identity> <certificate encodedValue="MIIDBTCCAfGgAwIBAgIQTdtbR9/uc7pOrPYli2WFnTAJBgUrDgMCHQUAMBoxGDAW BgNVBAMTD1dDRiBEZW1vIFNlcnZlcjAeFw0yMDAxMDEwNTAwMDBaFw00NTAxMDEw NTAwMDBaMBoxGDAWBgNVBAMTD1dDRiBEZW1vIFNlcnZlcjCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAMW6mtO0uSPREq8oH2wK6XcUpq2qDOEXzRNwHw2J Jyni9GWoWXw3JjKAqV8bnUo6OMVsXjtccL8EYo0kGhna91wUO2M4BVl66WPsXYJ+ WRRe+1TS9fBrBlBaZHKOXxEBoewbUQwoAnzZyFKVhwfLj9YhZQvORps7EhDvG31d BvtbSClQ5E2qfeXFGQrouyq68POEtETAgAN1DNxYSRVQNIdF/3BwEP1iUhyCFm/i VPjU+ftqSWgkAqKQkrQ5JPl1m1yfLfHAocHCxKDkAMxOPkpvxmu9sIKPRT1SfAiq 2mT9037Wb6PkCDczjVs/onVAmSddCQV8Vd9yF6XG+LPVMM0CAwEAAaNPME0wSwYD VR0BBEQwQoAQ3SsDBHimuFyUNHeE9rHzKKEcMBoxGDAWBgNVBAMTD1dDRiBEZW1v IFNlcnZlcoIQTdtbR9/uc7pOrPYli2WFnTAJBgUrDgMCHQUAA4IBAQDAAVOKrlz6 mxHbtclOAKbZbTxbm73koyj/de8qvxWO1e/q5s6A//uVqeJkbpffsxC9SXwEQby3 +IpK5yQgJCSNMrKzY5bAnqR5EPSpbP2WHON7FWwongg/rKuu5KUH+Zd/sd4PyCzR /CCAqUHfCtAktn3nd1YRVzbHhUFRUaxM0br4HuisMrlpkJrKEVgW8KMVZ0Pl9VX1 Fhx8BnzjCtayTEwmNilrd80YP9xRuXFifn+3xAl047+NsMBFr4/HnAYT0uco06S5 adGk671u8z3mXEO8c61C/CpbFPVqQC1eZ1JaaZHqhDd5D8ilzSBZRBhuF4jiaa/Z A8/VOhkoblaM" /> </identity> |
Client βTester to Web API Serviceβ Windows Form Application
The client βtester to serviceβ is a simple windows form application project (Client) in the same solution that connects to the βHello Authenticate Serviceβ by use of a ChannelFactory proxy to a specified service name that is defined in the client application configuration. The client program will use this proxy to test the OperationContract or methods available in the ServiceContract and return the results to the user on the form window. The project code is available on GitHub [here].
Features
Since the focus of the project demo is on the backend service, the client application was meant to only demonstrate consumption of the Hello Authenticate service and receive errors, so this client has limited scope of features. The authentication and operations are not done on the client, but on the service, in order to demo the service username and password validation and error handle features. Some of the client application features are:
Simple Design
The user interface is simplified to do basic demo, testing, verification, and showing results in the GUI.
Validation
The client validates the entry of the user data in the text boxes, before sending the request to the Hello Authenticate service. The username and password boxes must not be empty.
Error Handling
The client catches specific (DuplicateUserFault) and generic faults (GenericFault) sent from the service before it catches its own exceptions. It displays the fault (error) information to the user as a message box dialog window.
Results in the GUI
Successful and unsuccessful operations are communicated to the user with quick message boxes. The results of the GreetMe service request will be displayed in a text box in the same group. Likewise, the entire list of service callbacks (while the client was connected) will be displayed in a list box control in the callbacks group.
Program Code
The code behind file is for the client tester and manages the application in one file. The code is available on GitHub [here].
Fields
Private fields help the client with the following:
- Store their username and password (insecurely)
- Storing their registration and login states
- Storing all service callbacks and greeting messages received
Methods
Main
This main method is the entry point of the client tester application and does no special setup in this demo other than initialize the form components.
Events
Multiple button-click events handle the user requests to register, login, logoff, and get a greeting message. Some events may first validate user text input from the GUI.
Client to Service Communications Channel Setup
All of user-driven events communicate with the service by assembling a proxy using a DuplexChannelFactory class of type IHelloAuthenticateService specifying a configuration name as detailed in the client application configuration file. Client credentials (username and password) must be setup on the channel and added after the default configuration is removed. Β An example of configuration is shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
/// <summary> /// btnRegister_Click /// Event Handler for the user clicking on the button requesting /// to register with the serivice. /// </summary> /// <param name="sender">not used</param> /// <param name="e">not used</param> private void btnRegister_Click(object sender, EventArgs e) { try { // Validate and Parse the User Text Entry if (!ValidateReadUserDataEntry()) { return; } // Make a ChannelFactory Proxy to the Service DuplexChannelFactory<IHelloAuthenticateService> cf = new DuplexChannelFactory<IHelloAuthenticateService>(this, "myTcp"); #region How to Set Client Credentials on DuplexChannelFactory // First find and remove default endpoint behavior ClientCredentials defaultCredentials = cf.Endpoint.Behaviors.Find<ClientCredentials>(); cf.Endpoint.Behaviors.Remove(defaultCredentials); // Create New Credentials ClientCredentials credentials = new ClientCredentials(); credentials.UserName.UserName = username; credentials.UserName.Password = password; // Set new Credentials as new endpoint behavior on factory cf.Endpoint.Behaviors.Add(credentials); //add required ones #endregion How to Set Client Credentials on DuplexChannelFactory // Open the ChannelFactory Channel cf.Open(); // Create ChannelFactory Channel Proxy IHelloAuthenticateService proxy = cf.CreateChannel(); // Verify Proxy is Valid if (proxy != null) { // Try Communications try { // Call the Registration Service registered = proxy.Register(username, password); // User Registration Successful if (registered) { MessageBox.Show("Registration Successful", "Registration", MessageBoxButtons.OK); } // User Registration Failed else { MessageBox.Show("Registration Failed", "Registration", MessageBoxButtons.OK); } } // end of try // catch duplicate user fault catch (FaultException<DuplicateUserFault> ex) { MessageBox.Show("Error occurred: Duplicate User: " + ex.Reason, "FaultException<DuplicateUserFault> Caught"); } // catch generic faults catch (FaultException<GenericFault> ex) { MessageBox.Show("Error occurred: Generica Fault: " + ex.Reason, "FaultException<GenericFault> Caught"); } // Due to when the custom UserNamePasswordValidator is called in WCF, a fault cannot be sent securely from the // service as the user has not yet been authenticated and security established. // So, you will end up getting a MessageSecurityException with the following message: // "An unsecured or incorrectly secured fault was received from the other party. See the inner FaultException for the fault code and detail." // Since this is the only place we should get this error you can assume a bad login. // Also, the channel will be in a faulted state so kill it and create another! catch (MessageSecurityException) { // User is Not Logged In loggedin = false; MessageBox.Show("The username or password is incorrect.", "Invalid Credentials"); } // General Exception catch catch (Exception ex) { MessageBox.Show("Error sending message: " + ex.Message, "Error"); } } // end of if // Cannot Connect to Server else { MessageBox.Show("Cannot Create a Proxy Channel. Check Your Configuration Settings.", "Proxy", MessageBoxButtons.OK); } // end of else } catch (Exception ex) { MessageBox.Show("Unable to Create a Channel to the Service. Check your configuration settings." + ex.Message, "Channel Setup", MessageBoxButtons.OK); } } // end of method |
Methods
The only non-event driven method receives a callback message from the service (implements the IServiceCallback interface) and adds/updates the list of callbacks on the main UI thread.
Demo
A video demonstration of the project is available on YouTube here:
Screen captures are shown below for various user activities:
Startup
Registration
Registration Error
Login
Duplicate Login
Invalid Credentials
Logout
Logout Error
Greet Me Logged In
Greet Me Unregistered
Code
The entire project code repository is available on GitHub [here].