Click here to Skip to main content
15,861,172 members
Articles / Programming Languages / C# 4.0

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

Rate me:
Please Sign up or sign in to vote.
4.87/5 (37 votes)
23 Mar 2011CPOL7 min read 96.4K   4   96   17
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.

Image 1
(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:

C#
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):

XML
<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 <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.serviceauthenticationmanager.aspx">ServiceAuthenticationManager</a> and override the Authenticate method[3]; here if the CheckCredentials passes without any exception being thrown, then we authenticate the user from the credentials.

C#
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 ...
}
XML
<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:

C#
[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:

C#
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:

C#
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 ...
}
XML
<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.
C#
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.
C#
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.
C#
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.
C#
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).

C#
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].

C#
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).

C#
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:

C#
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:

C#
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.

C#
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:

XML
<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)


Written By
Architect Adrem Automation
Romania Romania
Motto: “Challenge is Life!”

Software architect, developer, project manager, consultant - depending on the “Challenge”.

challenge-me.ws





Don't forget to vote or share your comments.

Comments and Discussions

 
QuestionWon't load extension in Windows Service application Pin
e-horizon15-Dec-15 3:30
e-horizon15-Dec-15 3:30 
AnswerRe: Won't load extension in Windows Service application Pin
Alexandru Lungu15-Dec-15 21:51
professionalAlexandru Lungu15-Dec-15 21:51 
QuestionIs there a way to also encrypt the HTTP headers? Pin
Andre Silva8-Jul-15 8:08
Andre Silva8-Jul-15 8:08 
AnswerRe: Is there a way to also encrypt the HTTP headers? Pin
Alexandru Lungu8-Jul-15 22:47
professionalAlexandru Lungu8-Jul-15 22:47 
GeneralMy vote of 5 Pin
Molowe Hardom13-Dec-12 5:44
Molowe Hardom13-Dec-12 5:44 
GeneralGreat Article Pin
ozdemirv1-May-12 7:00
ozdemirv1-May-12 7:00 
GeneralRe: Great Article Pin
Alexandru Lungu2-May-12 2:32
professionalAlexandru Lungu2-May-12 2:32 
GeneralMy vote of 5 Pin
ozdemirv1-May-12 6:59
ozdemirv1-May-12 6:59 
GeneralRe: My vote of 5 Pin
Alexandru Lungu2-May-12 2:21
professionalAlexandru Lungu2-May-12 2:21 
QuestionMultiple server Pin
thangtx29-Sep-11 23:59
thangtx29-Sep-11 23:59 
QuestionEvent Callback? Pin
hulinning14-Sep-11 5:07
hulinning14-Sep-11 5:07 
AnswerRe: Event Callback? Pin
Alexandru Lungu15-Sep-11 10:10
professionalAlexandru Lungu15-Sep-11 10:10 
QuestionData contain unicode string Pin
thangtx6-Sep-11 18:00
thangtx6-Sep-11 18:00 
AnswerRe: Data contain unicode string Pin
Alexandru Lungu15-Sep-11 10:13
professionalAlexandru Lungu15-Sep-11 10:13 
GeneralRe: Data contain unicode string Pin
thangtx26-Sep-11 22:55
thangtx26-Sep-11 22:55 
GeneralMy vote of 5 Pin
b0bi24-May-11 9:18
b0bi24-May-11 9:18 
GeneralMy vote of 5 Pin
Ankit Rajput26-Apr-11 1:08
Ankit Rajput26-Apr-11 1:08 

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

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