Click here to Skip to main content
Click here to Skip to main content

WCF Client Server Application with Custom Authentication, Authorization, Encryption and Compression – Part 2

, 23 Mar 2011 CPOL
Rate this:
Please Sign up or sign in to vote.
HTTP - No IIS; Authentication - No SSL/X509 Certificate; Encryption - RSA+AES for Request, AES for Response; Compression - GZip for both Request/Response.

Table of Contents

This part presents in detail the implementation that due to its size was not treated in part 1.

Overview

We have 3 parts that will result in 3 projects – Client, Server and Common (common library for both the server and the client).

First, we take a quick look at the classes and I’ll explain what they do – though I think it is pretty obvious.


(Click on the diagram to enlarge)

Let’s start with the common part.
The ServerInfo contains the date/time and the public key of the server.
The Credentials contains UserName/Password/Expires properties that get serialized and sent to the server; the others (about date/time) are static properties initialized at the beginning from the server info and are used to set the Expires property just before the request.

Let’s move to the server at AppServer static class; its role is to create/start/stop the service that will serve to clients the ServerInfo.

Now on the client, we have AppClient static class; its role is to get the ServerInfo; it also hosts the client’s credentials.

Authentication

We start on the client where we add the Credentials to the message header; we do this by using a BehaviorExtensionElement that also implements IClientMessageInspector, like this:

public class ClientMessageInspector : BehaviorExtensionElement,
			IClientMessageInspector, IEndpointBehavior
{
    public object BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        request.Headers.Add(AppClient.Credentials.ToMessageHeader());
        return null;
    }
    //... other methods ...
}

and in the configuration file (App.config):

<behaviorExtensions>
  <add name="ChallengeClientMessageInspector"
	type="Challenge.Client.ClientMessageInspector, Client" />
</behaviorExtensions>

This is followed by the encoding mechanism where encryption and compression occurs, but we’ll talk about this later.

Now on the server: we need to derive a class from ServiceAuthenticationManager and override the Authenticate method[3]; here if the CheckCredentials passes without any exception being thrown, then we authenticate the user from the credentials.

public class ChallengeAuthenticationManager : ServiceAuthenticationManager
{
    public override ReadOnlyCollection<IAuthorizationPolicy>
	Authenticate(ReadOnlyCollection<IAuthorizationPolicy> authPolicy,
	Uri listenUri, ref Message message)
    {
        Credentials credentials = Credentials.FromMessageHeader(message);
        CheckCredentials(credentials);

        ChallengeIdentity identity = new ChallengeIdentity(credentials.UserName);
        IPrincipal user = new ChallengePrincipal(identity);
        message.Properties["Principal"] = user;

        return authPolicy;
    }

    public void CheckCredentials(Credentials credentials)
    {
        if (credentials.Expires < DateTime.Now)
            throw new AuthenticationException("Credentials expired!");

        // check the user and password against a database;
        // if not match
        // throw new AuthenticationException("Incorrect credentials!");
    }
    //... other methods ...
}
<behavior name="ChallengeBehavior">
  <serviceAuthenticationManager
   serviceAuthenticationManagerType=
	"Challenge.Server.ChallengeAuthenticationManager,
	Server"/>
</behavior>

Now let’s explain the ChallengeIdentity and ChallengePrincipal: first implements IIdentity and the later IPrincipal.

Both are extremely simple to implement; however I’ll insist below on the ChallengePrincipal because in it lays our authorization.

Authorization

With authorization, we can establish which of the authenticated users are allowed to execute the operation contract and the permission looks like this:

[PrincipalPermission(SecurityAction.Demand, Role = "Admin")]
public int Sum(int a, int b)
{
    return a + b;
}

The authenticated user will be allowed to execute the above method only if he is in the “Admin” role – this is checked using the IsInRole method:

public class ChallengePrincipal: IPrincipal
{
    IIdentity identity;
    string[] roles = null;

    public bool IsInRole(string role)
    {
        EnsureRoles();
        return roles != null ? roles.Contains(role) : false;
    }

    protected virtual void EnsureRoles()
    {
        // get the roles for the identity from a database (or other source)
        // and cache them for subsequent requests

        // here we'll add a few roles as example
        roles = new string[] { "User", "Admin", "Manager" };
    }
    //... other methods ...
}

In order to make the authorization work, we need to create our own authorization policy (and specify it in the configuration file) like this:

public class ChallengeAuthorizationPolicy : IAuthorizationPolicy
{
    public bool Evaluate(EvaluationContext evaluationContext, ref object state)
    {
        IPrincipal user = OperationContext.Current.IncomingMessageProperties
			["Principal"] as IPrincipal;
        evaluationContext.Properties["Principal"] = user;
        evaluationContext.Properties["Identities"] = new List<IIdentity>
				{ user.Identity };

        return false;
    }
    //... other methods ...
}
<behavior name="ChallengeBehavior">
  <serviceAuthorization principalPermissionMode="Custom" >
    <authorizationPolicies>
      <add policyType='Challenge.Server.ChallengeAuthorizationPolicy, Server' />
    </authorizationPolicies>
  </serviceAuthorization>
</behavior>

Encryption

Encryption/decryption is provided by common Cryptographer/ClientCryptographer/ServerCryptographer; a separate cryptographer for server and client is needed because they do different encryption/decryption.

Client - Encrypt Request
  • Get the id of the message, associate it with the key and keep them until the response from the server comes – we’ll need that key to decrypt the response from the server.
  • Get the element to encrypt (the Credentials element if only credentials are to be encrypted or the first node if the entire message will be encrypted) and encrypt that element.
  • Encrypt the AES key with the public key[2] of the server and add it to the encrypted node (using the name KeyElementName – which is a constant for both the server and the client).
  • Set the id of the encrypted element to the id of the message (to know the id of the message before decryption).
  • Replace the original node with the encrypted node.
public static void Encrypt(XmlDocument xmlDoc, string elementToEncrypt) //[8]
{
    XmlNodeList elementsToEncrypt = xmlDoc.GetElementsByTagName(elementToEncrypt);
    if (elementsToEncrypt.Count == 0)
        return;
    AesCryptoServiceProvider aesServiceProvider =
				new AesCryptoServiceProvider();
    aesServiceProvider.KeySize = 256;
    aesServiceProvider.GenerateKey();

    XmlNode idNode = xmlDoc.GetElementsByTagName("a:MessageID")[0];
    string id = idNode.InnerText;
    AesKeys.Add(id, aesServiceProvider.Key);

    XmlElement xmlElementToEncrypt = (XmlElement)elementsToEncrypt[0];
    EncryptedXml encryptedXml = new EncryptedXml();
    byte[] encryptedElement = encryptedXml.EncryptData
			(xmlElementToEncrypt, aesServiceProvider, Content);

    EncryptedData encryptedData = new EncryptedData();
    encryptedData.Type = EncryptedXml.XmlEncElementUrl;
    encryptedData.EncryptionMethod =
		new EncryptionMethod(EncryptedXml.XmlEncAES256Url);

    EncryptedKey encryptedKey = new EncryptedKey();
    encryptedKey.CipherData = new CipherData
     (EncryptedXml.EncryptKey(aesServiceProvider.Key, RsaServiceProvider, Content));
    encryptedKey.EncryptionMethod =
		new EncryptionMethod(EncryptedXml.XmlEncRSA15Url);
    encryptedData.KeyInfo = new KeyInfo();
    encryptedKey.KeyInfo.AddClause(new KeyInfoName(KeyElementName));
    encryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedKey));
    encryptedData.CipherData.CipherValue = encryptedElement;
    encryptedData.Id = id;
    EncryptedXml.ReplaceElement(xmlElementToEncrypt, encryptedData, Content);
}
Server - Decrypt Request
  • Consider the incoming document as an encrypted one;
  • Link the KeyElementName (which as I said before is constant for both the client and server) with the private key of the server that will be used to decrypt the client AES key;
  • Associate the id of the message to the key – this is needed to find the client key when encrypting the response;
  • Add the password to the ban list (or, if exists, throw a SecurityException);
  • Decrypt the document.
public static void Decrypt(XmlDocument xmlDoc) //[8]
{
    XmlNodeList encryptedElements = xmlDoc.GetElementsByTagName("EncryptedData");
    if (encryptedElements.Count == 0)
        return;

    EncryptedXml encryptedXml = new EncryptedXml(xmlDoc);
    encryptedXml.AddKeyNameMapping(KeyElementName, RsaServiceProvider);

    EncryptedData eData = new EncryptedData();
    XmlElement encryptedElement = (XmlElement)encryptedElements[0];
    eData.LoadXml(encryptedElement);

    SymmetricAlgorithm a = encryptedXml.GetDecryptionKey
		(eData, eData.EncryptionMethod.KeyAlgorithm);

    //here the aes service provider gets the client key![6]
    AesKeys.Add(eData.Id, a.Key);

    string keyHash = a.Key.ComputeHash();
    if (AesBannedKeys.ContainsKey(keyHash))
        throw new SecurityException("Password reuse before ban expiration!");
    else
        AesBannedKeys.Add(keyHash, DateTime.Now.AddMilliseconds
			(AesBannedKeysExpiresTimeSpan));

    encryptedXml.DecryptDocument();
}
Server - Encrypt Response
  • Find the element to encrypt.
  • Get the key based of the id of the message (RelatesTo element) that was saved from the request.
  • Encrypt the message.
  • Set the id of the message to the encrypted data (so that on the client to know which key to use – if only synchronous requests would be sent, then we’ll need just to use the last key, but we have to take both situations under consideration).
  • Replace the original element with the encrypted one.
public static void Encrypt(XmlDocument xmlDoc, string elementToEncrypt) //[9]
{
    XmlNodeList elementsToEncrypt = xmlDoc.GetElementsByTagName(elementToEncrypt);
    if (elementsToEncrypt.Count == 0)
        return;

    AesCryptoServiceProvider aesServiceProvider = new AesCryptoServiceProvider();

    XmlNode idNode = xmlDoc.GetElementsByTagName("a:RelatesTo")[0];
    string id = "";
    if (idNode != null)
    {
        id = idNode.InnerText;
    }
    if (AesKeys.ContainsKey(id))
    {
        aesServiceProvider.Key = AesKeys[id];
    }

    XmlElement xmlElementToEncrypt = (XmlElement)elementsToEncrypt[0];

    EncryptedXml encryptedXml = new EncryptedXml();
    byte[] encryptedElement = encryptedXml.EncryptData
		(xmlElementToEncrypt, aesServiceProvider, Content);

    EncryptedData encryptedData = new EncryptedData();
    encryptedData.Type = EncryptedXml.XmlEncElementUrl;
    encryptedData.EncryptionMethod = new EncryptionMethod
				(EncryptedXml.XmlEncAES256Url);
    encryptedData.CipherData.CipherValue = encryptedElement;
    encryptedData.Id = id;
    EncryptedXml.ReplaceElement(xmlElementToEncrypt, encryptedData, Content);
}
Client - Decrypt Response
  • Find the encrypted element and load it as an encrypted data.
  • Get the key to for the AES algorithm based on the id of the encrypted data.
  • Decrypt the encrypted element and replace it in the document.
public static void Decrypt(XmlDocument xmlDoc) //[9]
{
    XmlNodeList encryptedElements = xmlDoc.GetElementsByTagName("EncryptedData");
    if(encryptedElements.Count == 0)
        return;
    XmlElement encryptedElement = (XmlElement)encryptedElements[0];

    EncryptedData encryptedData = new EncryptedData();
    encryptedData.LoadXml(encryptedElement);

    AesCryptoServiceProvider aesServiceProvider = new AesCryptoServiceProvider();
    if (AesKeys.ContainsKey(encryptedData.Id))
    {
        aesServiceProvider.Key = AesKeys[encryptedData.Id];
    }

    EncryptedXml encryptedXml = new EncryptedXml();
    encryptedXml.ReplaceData(encryptedElement,
	encryptedXml.DecryptData(encryptedData, aesServiceProvider));
}

Since we want to do this in a message encoder, we don’t have the message as an XML document, but as an array segment; therefore, for client and server we need to do encryption on array segments (actually convert the array segments to XML documents and then apply the above methods).

public static ArraySegment<byte> EncryptBuffer(ArraySegment<byte> buffer,
	BufferManager bufferManager, int messageOffset,
	string elementToEncrypt = "s:Envelope")
{
    byte[] bufferedBytes;
    byte[] encryptedBytes;
    XmlDocument xmlDoc = new XmlDocument();

    using (MemoryStream memoryStream = new MemoryStream
		(buffer.Array, buffer.Offset, buffer.Count))
    {
        xmlDoc.Load(memoryStream);
    }

    ClientCryptographer.Encrypt(xmlDoc, elementToEncrypt);
    encryptedBytes = Encoding.UTF8.GetBytes(xmlDoc.OuterXml);
    bufferedBytes = bufferManager.TakeBuffer(encryptedBytes.Length);
    Array.Copy(encryptedBytes, 0, bufferedBytes, 0, encryptedBytes.Length);
    bufferManager.ReturnBuffer(buffer.Array);

    ArraySegment<byte> byteArray = new ArraySegment<byte>
	(bufferedBytes, messageOffset, encryptedBytes.Length);
    return byteArray;
}

Create a new AesCryptoServiceProvider[1] and generate a new key.

Compression

For compression/decompression, we will use Microsoft’s example; the decompression method is identical; I changed the compression because of a serious bug[7].

public static ArraySegment<byte> CompressBuffer
	(ArraySegment<byte> buffer, BufferManager bufferManager, int messageOffset)
{
    byte[] bufferedBytes, compressedBytes;
    using (MemoryStream memoryStream = new MemoryStream())
    {
        memoryStream.Write(buffer.Array, 0, messageOffset);

        using (GZipStream gzStream = new GZipStream
		(memoryStream, CompressionMode.Compress, true))
        {
            gzStream.Write(buffer.Array, messageOffset, buffer.Count);
        }

        compressedBytes = memoryStream.ToArray();
        bufferedBytes = bufferManager.TakeBuffer(compressedBytes.Length);

        Array.Copy(compressedBytes, 0, bufferedBytes, 0, compressedBytes.Length);
        bufferManager.ReturnBuffer(buffer.Array);
    }
    //ArraySegment<byte> byteArray = new ArraySegment<byte>
    //(bufferedBytes, messageOffset, bufferedBytes.Length - messageOffset);//bug here
    ArraySegment<byte> byteArray =
	new ArraySegment<byte>(bufferedBytes, messageOffset, compressedBytes.Length);
    return byteArray;
}

Encoding

This might be not so easy to understand by just following the attached code; so I’ll present it with a few changes to make it easier to understand.

First, I’ll show only the encoding done for the client (there is no point in showing the server because it is similar); and second - I’m combining the common with the client (because actually all started this way – thinking at the client, then at the server and after that, at their identical parts and thus the “common” project came into being).

First, we have the ClientEncoder based on the abstract class MessageEncoder with the most important methods WriteMessage and ReadMessage; here we use the methods presented in the Encryption and Compression chapters; the ContentCompression and ContentEncryption properties are initialized by the class that creates the encoder and are actually a propagation of the contentCompression and contentEncryption attributes from the configuration file (a little later, we’ll see how this is retrieved from the App.config).

public class ClientMessageEncoder : MessageEncoder //[4]
{
    public override ArraySegment<byte> WriteMessage(Message message,
	int maxMessageSize, BufferManager bufferManager, int messageOffset)
    {
        ArraySegment<byte> buffer = innerEncoder.WriteMessage
		(message, maxMessageSize, bufferManager, messageOffset);
        switch (ContentEncryption)
        {
            case ContentEncryptionType.All:
                {
                    buffer = ClientCryptographer.EncryptBuffer(buffer,
				bufferManager, messageOffset);
                    break;
                }
            case ContentEncryptionType.Credentials:
                {
                    buffer = ClientCryptographer.EncryptBuffer(buffer,
				bufferManager, messageOffset,
				ContentEncryptionType.Credentials.ToString());
                    break;
                }
        }

        if (ContentCompression != ContentCompressionType.None)
            buffer = CompressBuffer(buffer, bufferManager, messageOffset);

        return buffer;
    }

    public override Message ReadMessage(ArraySegment<byte> buffer,
		BufferManager bufferManager, string contentType)
    {
        ArraySegment<byte> workingBuffer = buffer;

        if (ContentCompression != ContentCompressionType.None)
            buffer = DecompressBuffer(buffer, bufferManager);

        if (ContentEncryption != ContentEncryptionType.None)
            buffer = ClientCryptographer.DecryptBuffer(buffer, bufferManager);

        Message returnMessage = innerEncoder.ReadMessage(buffer, bufferManager);
        returnMessage.Properties.Encoder = this;
        return returnMessage;
    }
    //... other methods ...
}

The ClientMessageEncoder is used by a factory encoder:

public class ClientMessageEncoderFactory : MessageEncoderFactory //[13]
{
    MessageEncoder encoder;
    public ClientMessageEncoderFactory(MessageEncoderFactory messageEncoderFactory)
    {
        encoder = new ClientMessageEncoder(messageEncoderFactory.Encoder);
    }
    //... other methods ...
}

The ClientMessageEncoderFactory is used by the binding element in the CreateMessageEncoderFactory method:

public class ClientMessageEncodingBindingElement :
			MessageEncodingBindingElement //[12]
{
    public override IChannelFactory<TChannel>
	BuildChannelFactory<TChannel>(BindingContext context)
    {
        context.BindingParameters.Add(this);
        var property = GetProperty<XmlDictionaryReaderQuotas>(context);
        property.MaxStringContentLength = Int32.MaxValue; // [14]
        return context.BuildInnerChannelFactory<TChannel>();
    }

    public override MessageEncoderFactory CreateMessageEncoderFactory()
    {
        ClientMessageEncoderFactory factory =
		new ClientMessageEncoderFactory
		(innerBindingElement.CreateMessageEncoderFactory());
        ClientMessageEncoder encoder = factory.Encoder as ClientMessageEncoder;
        encoder.ContentCompression = ContentCompression;
        encoder.ContentEncryption = ContentEncryption;
        return factory;

    }
    //... other methods ...
}

And the above binding element it is used by the extension element; here we can get attributes from the configuration (like contentEncryption) and from here we propagate them to the message encoder.

public class ClientMessageEncodingElement : BindingElementExtensionElement
{
    [ConfigurationProperty("contentEncryption", DefaultValue = "Credentials")]
    public string ContentEncryption
    {
        get { return (string)base["contentEncryption"];}
        set { base["contentEncryption"] = value; }
    }
    protected override BindingElement CreateBindingElement()
    {
        ClientMessageEncodingBindingElement bindingElement =
			new ClientMessageEncodingBindingElement();
        this.ApplyConfiguration(bindingElement);
        return bindingElement;
    }
    //... other methods ...
}

And this is how the config should look like:

<extensions>
  <bindingElementExtensions>
    <add name="ClientMessageEncoding"
	type="Challenge.Client.ClientMessageEncodingElement, Client" />
  </bindingElementExtensions>
</extensions>
    ...
<bindings>
  <customBinding>
    <binding name="ChallengeMessageEncoding">
      <ClientMessageEncoding contentEncryption="All" contentCompression="GZip" />
      <httpTransport/>
    </binding>
  </customBinding>
</bindings>

Now, that you know "how it is made", you can start using the code and then implement extensions.

Notes

  1. The solution uses .NET 4.0.
  2. Please ignore the warnings related to the source control (TFS).
  3. You may need to re-add some of the references: System.Configuration, System.ServiceModel, System.IdentityModel, System.Security, System.Runtime.Serialization, Ionic.Zip (the last is in the Common project; the others come with .NET 4).
  4. The references are for both the parts of the article and for the attached code (comments with numbers).
  5. You need administrative rights to start the server.
  6. To use Microsoft’s implementation of zip and remove the need for Ionic.Zip.dll, just change in the Common Project, Encoding.cs the “using Ionic.Zlib;” with “using System.IO.Compression;”; then you can delete the reference and the DLL.
  7. To avoid duplication, the code is attached only to the first part of the article.
  8. If you want to test directly the server.exe and the client.exe from the attached zip, you need to unblock first server.exe.config and client.exe.config (right click, properties, unblock); otherwise you'll get a configuration error.

References

[1] RSACryptoServiceProvider Info
[2] AesCryptoServiceProvider Info
[3] Custom WCF authentication
[4] Custom Message Encoder: Custom Text Encoder
[5] Cryptography Helper
[6] How to Get the AES Encryption Key from a RSA+AES Encrypted XML
[7] WCF GZip Compression Bug
[8] How to: Encrypt XML Elements with Asymmetric Keys
[9] How to: Encrypt XML Elements with Symmetric Keys
[10] The Universal Code Breaker
[11] WCF ClearUsernameBinding
[12] Microsoft samples
[13] Microsoft code - Encoder/Factory
[14] Resolving XmlDictionaryReaderQuotas Error for WCF Compression using GZipEncoder with Custom Binding
[15] Man in the middle attack

History

  • 2011-03-08 Version 1.0.0 - Initial post
  • 2011-03-18 Version 1.1.0 - Added the code with the password ban list
  • 2011-03-24 Version 1.1.1 - Small text changes

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Alexandru Lungu
Architect Adrem Automation
Romania Romania
Alexandru Lungu graduated in 2004, first in his class, Cybernetics, Statistics and Computer Science faculty from A.S.E. Bucharest, and two years later finished his master in project management.
 
His motto: “Challenge is Life!”
 
Now, he works as project manager, developer, consultant depending on the “Challenge”.
 
You can reach him at challenge-me.ws
 

 

 
Don't forget to vote or share your comments.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberMolowe Hardom13-Dec-12 5:44 
GeneralGreat Article Pinmemberozdemirv1-May-12 7:00 
GeneralRe: Great Article PinmemberAlexandru Lungu2-May-12 2:32 
GeneralMy vote of 5 Pinmemberozdemirv1-May-12 6:59 
GeneralRe: My vote of 5 PinmemberAlexandru Lungu2-May-12 2:21 
QuestionMultiple server Pinmemberthangtx29-Sep-11 23:59 
QuestionEvent Callback? Pinmemberlinhjob14-Sep-11 5:07 
AnswerRe: Event Callback? PinmemberAlexandru Lungu15-Sep-11 10:10 
QuestionData contain unicode string Pinmemberthangtx6-Sep-11 18:00 
AnswerRe: Data contain unicode string PinmemberAlexandru Lungu15-Sep-11 10:13 
GeneralRe: Data contain unicode string Pinmemberthangtx26-Sep-11 22:55 
GeneralMy vote of 5 Pinmemberb0bi24-May-11 9:18 
GeneralMy vote of 5 PinmemberAnkit Rajput26-Apr-11 1:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.141022.2 | Last Updated 24 Mar 2011
Article Copyright 2011 by Alexandru Lungu
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid