.NET

Secure WCF Services with custom encrypted tokens

Windows Communication Foundation framework comes with a lot of options out of the box, concerning the security logic you will apply to your services. Different bindings can be used for certain kind and levels of security. Even the BasicHttpBinding binding supports some types of security. There are some times though where you cannot or don’t want to use WCF security available options and hence, you need to develop your own authentication logic accoarding to your business needs. This post will show you how to pass encrypted information from the client to server using a custom header, with the latter (server) decrypting and authenticating each request. I wanted to post about this since I tempted to invoke Amazon Product Advertizing API service methods. In a nutchell, to use this service, you need to create an ancrypted token which encapsulates your amazon account data and pass it through a custom header. There maybe some implementations out there to do it and Amazon itself has several tutorials to help you, but this post will go a little further and show you the full cycle of authenticating WCF requests using this technique. Here’s what we gonna see on this post:

  1. Create an Authentication Behavior attribute
  2. Apply the attribute into the Service
  3. Authenticate requests based on that attribute
  4. Create clients who have reference to that behavior attribute
  5. Create externals clients who do not have reference to that behavior attribute

Let’s start the action. Create a blank solution and add a new class library project named AuthBehavior. First thing you need to do is to create your header data. These are the contracts which will encaptulate authentication data such as username, password, etc.. Generally speaking, you only need a class to hold your authentication data and another one to be used for holding those data in an encrypted format. The latter one has only one propety named EncryptedSignature. Create a folder “Contracts”, add an AuthenticationHeader class file and paste the following code.

namespace AuthBehavior
{
    [DataContract(Namespace = "http://chsakell.com")]
    public class AuthenticationHeader
    {
        [DataMember]
        public string EncryptedSignature { get; set; }
    }
    public class AuthenticationData
    {
        public string Username { get; set; }
        public string Password { get; set; }
        public string Timespan { get; set; } // Seed data
    }
}

You will have noticed that I added a Timespan property in the AuthenticationData class. I added this so that each time you send your auth data object encrypted, since the Timespan property which is equal to DateTime.UtcNow, the encrypted signature will be always different. That’s why I called it the Seed data. More over, this timespan value will be valid only if it’s between a certain padding in minutes from server’s DateTime.UtcNow. The purpose of these two classes, is to force the client to pass a request message such as this:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <authentication-header xmlns="chsakell.com" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <EncryptedSignature xmlns="http://chsakell.com">A2+WsZ1D89qybIM++hIgXRzViNGMBsGwL1jc2I16a8EIVRBSXCoTQCO3DJQ=</EncryptedSignature>
    </authentication-header>
    <To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://localhost:9001/BlogService</To>
    <Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IBlogService/ReadPosts</Action>
  </s:Header>
  <s:Body>
    <ReadPosts xmlns="http://tempuri.org/" />
  </s:Body>
</s:Envelope>

Create a Behaviors folder and add a C# class file named ClientAuthenticationHeaderContext. This class will be used from clients who have reference to this library. We will see later how to use this.

public class ClientAuthenticationHeaderContext
    {
        public static AuthenticationData HeaderInformation;
        static ClientAuthenticationHeaderContext()
        {
            HeaderInformation = new AuthenticationData();
        }
    }

Now it’s time to get serious. If you need to add this custom authentication logic to your service, you need to create an apply a specific attribute to it. This attribute must implement the following interfaces:

  1. IDispatchMessageInspector
  2. IClientMessageInspector
  3. IEndpointBehavior
  4. IServiceBehavior

I created this attribute in the Behaviors folder but since it’s quite big to paste it here, I will only show you the most important parts. You don’t have to worry about the code cause as always at the end of this post you will find a link to download this project.

// IDispatchMessageInspector ovveride
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            try
            {
                var header = request.Headers.GetHeader<AuthenticationHeader>("authentication-header", "chsakell.com");
                if (header != null)
                {
                    string headerPassword = string.Empty;
                    string headerUsername = string.Empty;
                    string headerTimeSpan = string.Empty;
                    try
                    {
                        string decryptedSignature = Encryption.Decrypt(header.EncryptedSignature, true);
                        AuthenticationData headerData = Serializer.JsonDeserialize<AuthenticationData>(decryptedSignature);
                        headerUsername = headerData.Username;
                        headerPassword = headerData.Password;
                        headerTimeSpan = headerData.Timespan;
                    }
                    catch
                    {
                        throw new UnauthorizedAccessException("Unable to decrypt signature");
                    }
                    if (!string.IsNullOrEmpty(headerPassword) && (!string.IsNullOrEmpty(headerUsername)) && (!string.IsNullOrEmpty(headerTimeSpan)))
                    {
                        if (IsRequestValid(headerPassword, headerUsername, headerTimeSpan))
                            return null;
                        else
                            throw new UnauthorizedAccessException("Wrong credentials");
                    }
                    else
                    {
                        throw new MessageHeaderException("Missing credentials from request");
                    }
                }
                else
                    throw new MessageHeaderException("Authentication header not found");
            }
            catch (UnauthorizedAccessException ex)
            {
                throw new FaultException(ex.Message);
            }
            catch (MessageHeaderException ex)
            {
                throw new FaultException(ex.Message);
            }
            catch (Exception ex)
            {
                throw new FaultException(ex.Message);
            }
        }

IsRequestValid

/// <summary>
        /// Authenticate request. Use Database for checking user credentials instead
        /// </summary>
        /// <param name="headerPassword"></param>
        /// <param name="headerUsername"></param>
        /// <param name="requestTimeUTCCreated"></param>
        /// <returns></returns>
        private bool IsRequestValid(string headerPassword, string headerUsername, string requestTimeUTCCreated)
        {
            bool isAuthenticatedRequest = new bool();
            try
            {
                string authUsername = "chris";
                string authPassword = "sakell";
                DateTime utcNow = DateTime.UtcNow;
                DateTime requestSent = DateTime.ParseExact(requestTimeUTCCreated, "yyyy-MM-ddTHH:mm:ssZ",
                    System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime();
                int timeDiffInMinutes = (int)utcNow.Subtract(requestSent).TotalMinutes;
                // Three prerequisites so that a request is valid - notice the 5 minutes padding
                if (headerPassword == authPassword && headerUsername == authUsername && Math.Abs(timeDiffInMinutes) < 5)
                    isAuthenticatedRequest = true;
                return isAuthenticatedRequest;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

Apply this attribute into the Service implementation class as follow:

[AuthenticationInspectorBehavior]
public class BlogService : IBlogService

This code is invoked at the server side level when a request reaches. If the request pass successfully this part then the requested Service action will be called, otherwise a FaultException error will return to client. The code above searches for the required authentication header and if it finds, tries to decrypt, deserialize the signature and procceed with the authentication. If any of those steps fails, then the request is marked as invalid and an exception is returned to the client. Let’s check it on action:

wcf-afterreceiverequest

You can see that the required header has been found and the signature has been decrypted and deserialized. From the Debug Immediate window you can see the deserialized auth data as well. The service will procceed with the actuall authentication (ideally in a database) and will return null in case of success, otherwise it will thrown an exception.

Now let’s see, how clients that reference this library can automatically generate an authentication token and pass it through a message header. This is done through the BeforeSendRequest of the IClientMessageInspector interface.

// IClientMessageInspector ovveride
public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            DateTime now = DateTime.UtcNow;
            string timestamp = now.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
            AuthenticationData authData = new AuthenticationData
            {
                Username = ClientAuthenticationHeaderContext.HeaderInformation.Username,
                Password = ClientAuthenticationHeaderContext.HeaderInformation.Password,
                Timespan = timestamp // This is the seed..
            };
            string serializedAuthData = Serializer.JsonSerializer<AuthenticationData>(authData);
            string signature = string.Empty;
            signature = Encryption.Encrypt(serializedAuthData, true);
            var encryptedHeader = new AuthenticationHeader
            {
                EncryptedSignature = signature
            };
            var typedHeader = new MessageHeader<AuthenticationHeader>(encryptedHeader);
            var untypedHeader = typedHeader.GetUntypedHeader("authentication-header", "chsakell.com");
            request.Headers.Add(untypedHeader);
            return null;
        }

I used some sample code for Encryption – Serialization I found on the internet (so let’s give those folks a gredit) but you can use your own implementations as well.

* [You will see at the comments of this post, that a fellow has suggested the use of an assymetric key based algorithm for the signature encryption/decryption, which is quite a good idea.]

Let’s explain how this code works in action. When you create proxy clients that have reference to this library, the only thing you need to do is to add this behavior attribute to the client’s endpoint behaviors collection and set the ClientAuthenticationHeaderContext.HeaderInformation data. Doing that, before the actual request leaves the client, an authentication message header will be build and encapsulated into the request. This is how you create such clients:

private static BlogServiceClient CreateBlogServiceClient(string username, string password)
        {
            BlogServiceClient blogServiceClient = null;
            System.ServiceModel.BasicHttpBinding basicHttpbinding = new System.ServiceModel.BasicHttpBinding(System.ServiceModel.BasicHttpSecurityMode.None);
            basicHttpbinding.Name = "BasicHttpBinding_IBlogService";
            basicHttpbinding.MaxReceivedMessageSize = 2147483646;
            basicHttpbinding.MaxBufferSize = 2147483646;
            basicHttpbinding.MaxBufferPoolSize = 2147483646;
            basicHttpbinding.ReaderQuotas.MaxArrayLength = 2147483646;
            basicHttpbinding.ReaderQuotas.MaxStringContentLength = 5242880;
            basicHttpbinding.SendTimeout = new TimeSpan(0, 5, 0);
            basicHttpbinding.CloseTimeout = new TimeSpan(0, 5, 0);
            basicHttpbinding.Security.Mode = BasicHttpSecurityMode.None;
            basicHttpbinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
            System.ServiceModel.EndpointAddress endpointAddress = new System.ServiceModel.EndpointAddress(hostURI);
            blogServiceClient = new BlogServiceClient(basicHttpbinding, endpointAddress);
            blogServiceClient.ChannelFactory.Endpoint.Behaviors.Add(new AuthenticationInspectorBehavior());
            ClientAuthenticationHeaderContext.HeaderInformation.Username = username;
            ClientAuthenticationHeaderContext.HeaderInformation.Password = password;
            return blogServiceClient;
        }

But what if you have a client that hasn’t reference to this library? No problem at all. This client will certainly need to create the same contracts at least. Hence, he needs to the same AuthenticationHeader and AuthenticationData classes in to his project. More over he must serialize and encrypt the authentication data with the same algorithms, otherwise the service won’t be able to decrypt and deserialize them. When all these are ready and after the proxy client is created (without the custom behavior as previous) this is what it remains to be done:

static void ReadPosts()
        {
            try
            {
                // Create a simple client
                BlogServiceClient blogClient = CreateBlogServiceClient();
                using (new OperationContextScope(blogClient.InnerChannel))
                {
                    var headerData = GenerateEncryptedHeader("chris", "sakell");
                    MessageHeader aMessageHeader = MessageHeader.CreateHeader("authentication-header", "chsakell.com", headerData);
                    OperationContext.Current.OutgoingMessageHeaders.Add(aMessageHeader);
                    List<Post> blogPosts = blogClient.ReadPosts();
                    foreach (Post post in blogPosts)
                    {
                        Console.WriteLine(post.Title);
                    }
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
private static AuthenticationHeader GenerateEncryptedHeader(string username, string password)
        {
            try
            {
                // Prepare authentication data to send
                AuthenticationData authData = PrepareAuthenticationData(username, password);
                // Serialize data
                string serializedAuthData = Serializer.JsonSerializer<AuthenticationData>(authData);
                // Encrypt data
                string signature = Encryption.Encrypt(serializedAuthData, true);
                var encryptedHeader = new AuthenticationHeader
                {
                    EncryptedSignature = signature
                };
                return encryptedHeader;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        private static AuthenticationData PrepareAuthenticationData(string username, string password)
        {
            try
            {
                // This will be the seed..
                DateTime now = DateTime.UtcNow;
                string timestamp = now.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
                AuthenticationData authData = new AuthenticationData
                {
                    Username = username,
                    Password = password,
                    Timespan = timestamp
                };
                return authData;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

When you download this project you will find that I have created a BlogService WCF service with a singe operation ReadPosts(). This Service is inside a class library project named BlogService.Lib and is self-hosted in a console application named BlogService.Host. You will also find two other console applications, one for internal clients named BlogService.InternalClient. that have reference to the behavior library and another one named BlogService.ExternalClient. In order to run the solution, you need to have administrative rights so you can self-host the WCF service. After running the host project, start the clients. Download the project from here here.

Food for thought

You may be wondering if this is the best option for securing your WCF services without using the out of the box options. I would say certainly not, but this is quite better than nothing. If you haven’t a really good reason not to use WCF options (see https, message/transport encryption, certificates) well then this is a elegant way to go with. That’s it, I hope you enjoyed the post!

Reference: Secure WCF Services with custom encrypted tokens from our NCG partner Christos Sakellarios at the chsakell’s Blog blog.

Christos Sakellarios

Senior Software Engineer, Blogger

Related Articles

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button