Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Custom Authentication and Security for Routing Service of WCF 4.0

4.50/5 (4 votes)
13 Oct 2012CPOL8 min read 45.3K   1.3K  
Defining security contexts in Routing service environment
Though WCF routing features for content-based routing, protocol bridging, and error handling seem to be very exciting, I have faced some issues while defining their security context in Routing service environment. In this article, we will discuss custom authentication and security for routing services.

Introduction

I came across WCF 4 routing features while designing some Central services which will provide various services to all of my client-end service. I like to create a single entry point to communicate all those services and keep them separate because of scalability reason. WCF routing feature seems to be very exciting which supports a configurable routing service that you can use in your WCF solutions. It provides features for content-based routing, protocol bridging, and error handling. But I have faced some issues there while defining their security context in Routing service environment.

Background

Image 1

Nowadays, I am building such architecture where ‘Service L’ in customer domain end will provide some services to Client X, Y, Z and so on.. workstations. ‘Service L’ has lots of things to provide its clients (like data from databases, etc.).

And the interesting thing is that in some cases, it will consume some services from the Provider End which have been distributed in several independent backend services like ‘Service A’, ‘Service B’. Since those services in Provider domain are not available outside, it has a routing service ‘Router service’ that gives all ‘Service L’ Clients to access the backend services and it acts like a bridge to consume them. So all ‘Service L’ going to become as a client to Routing service which sends a request to it over the internet and it is going to route them to the appropriate backend service based on filter/policy.

The security between Service L and Client X/Y/Z has already been established and I am not going to talk about that part. I am very much concerned about the rest of the parts I have, the security in the routing environment.

This routing part has been developed based on WCF 4.0 routing feature. But I didn’t find any good solution for the security context when forwarding the messages to back end services.

Image 2

Transport-Level and Message-Level Security

WCF allows us to enable security at two levels: the transport level or the message level. Let me explain the difference between these two levels here:

If we use transport-level security, then the complete communication (all the information exchanged between the client and the server) would be encrypted.

Image 3

If we use message-level security, then only the content of the SOAP message is encrypted, while the rest of the SOAP message is left unencrypted.

Image 4

So in the configuration file, while writing your binding configuration:

  • Using Security mode=Message means the message (or parts of it) are encrypted and signed and can be sent over unsecured transports.
  • Using Security mode=Transport means that the communication channel is encrypted rather than just the message.

Transport is faster but means the data is only secure while on the wire - if you had to use intermediaries (like the WCF 4 routing service), then it would be able to see the message in the clear - with message security, the message is secure from sender to the receiver. Transport-level and message-level security can assurance isolation, reliability, and certification. However, not all communications need to have those three features all at once. Security is usually desirable, but can be disabled. Encryption can also be activated to ensure privacy.

Why Not Transport Security?!!

WCF routing is based on message level rather than transport layer routing. Therefore, authentication at transport layer cannot be forwarded correctly by the router and there isn't a direct means to make those security assertion flows from client to back end (bypass the Router Service). HTTPS/SSL like transport layer solution which can be used only for point to point rather than end to end case. So you cannot establish HTTPS/SSL connection across multiple nodes (client, router and server)

Here, the routing service like Consumer --> Routing Service --> Backend Service, then I need to make sure that there is transport security enabled between:

  • Consumer -> Routing Service
  • Routing Service -> Backend Service

According to that, the message will be encrypted on transport from the consumer to the routing service. When the message enters the routing service, it will be decrypted, when sending the message to the back end service, it will be encrypted again on transport and decrypted at the backend service. So the message will be encrypted and decrypted twice in such solution. But since there are intermediary systems between client and the service; each intermediate point must forward the message over a new SSL connection. So Transport security is not appropriate when:

  • you are sending a message directly from your application to a WCF service and the message will not be routed through intermediate systems
  • Both the service and the client are located other than an intranet.

So I am going to avoid Transport security in my scenario.

Why Not Message Security?!!

Message security is transport-independent and therefore can be used with any transport protocol. When you use message based security, this is done on the SOAP message (so the encryption takes place on service level instead of transport level) which gives an End-to-End security solution. So the consumer sends an encrypted message (header is not encrypted default) and the Routing Service just routes the encrypted message to the backend service and the backend service decrypts the message. So message security directly encrypts and signs the message, having intermediaries does not break the security. But Message based security gives you a performance penalty in comparison to Transport Security, because the message becomes way larger after encrypting it.

So message security with Windows credentials in Router and backend service will work since support of delegation (Kerberos). And Client needs the access to that identity object. On the other hand, it doesn’t also allow me to forward security context for:

  • Username and Password
  • X.509
  • Federated credentials

It means that the router can be configured to enforce message security but the service must be configured to disable security and it cannot access the security context such as the user's Identity.

So, What I Am Going to Do

  1. Add extension of endpoint behavior to add headers to send User name and password (can be sent in encrypted way too).
  2. Create a custom binding to encrypt and compression message in between Client and Routing Service.
  3. Use Regular security in binding while forwarding message to back-end service from Routing service.
  4. Create an authentication manager to do authentication by user name and password (against database) to avoid anonymous calls.

Add Extension of Endpoint Behavior to Add Headers to Send User Name and Password

So let's define the extension of endpoint behavior here:

C#
public class CentralSessionEndpointBehavior : IEndpointBehavior
{
    public CentralSessionEndpointBehavior()
    {

    }

    public CentralSessionEndpointBehavior(string user, string password)
    {
        this.User = user;
        this.Password = password;
    }

    public void AddBindingParameters(ServiceEndpoint serviceEndpoint, 
       System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    { }

    public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint, 
      System.ServiceModel.Dispatcher.ClientRuntime behavior)
    {
        //Add the inspector
        behavior.MessageInspectors.Add(new CentralSessionClientMessageInspector
                                      (this.User, this.Password));
    }
    public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint, 
      System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
    { }

    public void Validate(ServiceEndpoint serviceEndpoint)
    { }

    public string User { get; set; }
    public string Password { get; set; }
}

public class CentralSessionClientMessageInspector : 
            BehaviorExtensionElement, IClientMessageInspector
{
    public CentralSessionClientMessageInspector()
    {

    }

    public CentralSessionClientMessageInspector(string user, string password)
    {
        this.User = user;
        this.Password = password;
    }

    public static event EventHandler<CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message>> PreRequestingService;

    private void InvokePreRequestingService(CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message> e)
    {
        EventHandler<CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message>> handler = PreRequestingService;
        if (handler != null) handler(this, e);
    }

    public static event EventHandler<CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message>> PostRequestingService;

    private void InvokePostRequestingService(CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message> e)
    {
        EventHandler<CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message>> handler = PostRequestingService;
        if (handler != null) handler(this, e);
    }

    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
    private string _user;

    public string User
    {
        get { return _user; }
        set { _user = value; }
    }

    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
    private string _password;

    public string Password
    {
        get { return _password; }
        set { _password = value; }
    }

    public override Type BehaviorType
    {
        get { return typeof(CentralSessionEndpointBehavior); }
    }

    protected override object CreateBehavior()
    {
        return new CentralSessionEndpointBehavior();
    }

    #region IClientMessageInspector Members
    public void AfterReceiveReply
        (ref System.ServiceModel.Channels.Message reply, object correlationState)
    {
        InvokePostRequestingService(new CentralSessionDataEventArgs<Message>(reply));
    }
    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, 
           System.ServiceModel.IClientChannel channel)
    {
        CredentialHelper.SetSessionData(_user, _password, ref request);
        InvokePreRequestingService(new CentralSessionDataEventArgs<Message>(request));
        return null;
    }

    #endregion
}

public class CentralSessionDataEventArgs<TData> : EventArgs where TData : class
{
    readonly TData _data;

    public CentralSessionDataEventArgs(TData data)
    {
        if (data == null)
        {
            throw new ArgumentNullException("data");
        }
        this._data = data;
    }

    public TData Data
    {
        get { return _data; }
    }

    public override string ToString()
    {
        return _data.ToString();
    }
}

Here, I going to send the user name and password in BeforeSendRequest:

C#
public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, 
              System.ServiceModel.IClientChannel channel)
{
    CredentialHelper.SetSessionData(_user, _password, ref request);
    InvokePreRequestingService(new CentralSessionDataEventArgs<Message>(request));
    return null;
} 

The SetSessionData method will add the user name and password in each request sent from the client to Routing service. Here is the definition of the CredentialHelper class:

C#
public class CredentialHelper
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private const string HnForUserName = "UserName";

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private const string HNamespaceForUserName = @"http://UserName.url";

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private const string HnForPassword = "Password";

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private const string HNamespaceForPassword = @"http://Password.url";

    public static CentralRequestSession GetSessionData(Message request)
    {
        var user = string.Empty;
        var pass = string.Empty;
        if (request.Headers.Any(h => h.Name.Equals(HnForUserName)))
            user = request.Headers.GetHeader<string>
                   (HnForUserName, HNamespaceForUserName, HnForUserName);
        if (request.Headers.Any(h => h.Name.Equals(HnForPassword)))
            pass = request.Headers.GetHeader<string>
                   (HnForPassword, HNamespaceForPassword, HnForPassword);
        return new CentralRequestSession(user, pass);
    }

    public static void SetSessionData
           (string userName, string password, ref  Message request)
    {
        var userHeader = new MessageHeader<string> 
                         { Actor = HnForUserName, Content = userName };
        //Creating an untyped header to add to the WCF context
        System.ServiceModel.Channels.MessageHeader unTypedHeaderForUser = 
          userHeader.GetUntypedHeader(HnForUserName, HNamespaceForUserName);
        //Add the header to the current request
        request.Headers.Add(unTypedHeaderForUser);

        var passwordHeader = new MessageHeader<string> 
                             { Actor = HnForPassword, Content = password };
        //Creating an untyped header to add to the WCF context
        System.ServiceModel.Channels.MessageHeader unTypedHeaderForPassword = 
          passwordHeader.GetUntypedHeader(HnForPassword, HNamespaceForPassword);
        //Add the header to the current request
        request.Headers.Add(unTypedHeaderForPassword);
    }
}

Create a Custom Binding to Encrypt and Compression Message in Between Client and Routing Service

To compress the message over the wire, I have used the Gzip from the sample provided by Microsoft. And I made some changes to add code for Encryption/Decryption there. So here, I am going to highlight those portions of code.

So at both ends, client and routing service end, I need to encrypt the data first and then compress it while sending the data across wire:

C#
//Helper method to compress an array of bytes
static ArraySegment<byte> CompressBuffer(ArraySegment<byte> buffer, 
  BufferManager bufferManager, int messageOffset, CompressionAlgorithm compressionAlgorithm)
{
    buffer = Cryptographer.EncryptBuffer(buffer, bufferManager, messageOffset);

    var memoryStream = new MemoryStream();
    
    using (Stream compressedStream = compressionAlgorithm == CompressionAlgorithm.GZip ? 
        (Stream)new GZipStream(memoryStream, CompressionMode.Compress, true) :
        (Stream)new DeflateStream(memoryStream, CompressionMode.Compress, true))
    {
        compressedStream.Write(buffer.Array, buffer.Offset, buffer.Count);
    }

    byte[] compressedBytes = memoryStream.ToArray();
    int totalLength = messageOffset + compressedBytes.Length;
    byte[] bufferedBytes = bufferManager.TakeBuffer(totalLength);

    Array.Copy(compressedBytes, 0, bufferedBytes, messageOffset, compressedBytes.Length);

    bufferManager.ReturnBuffer(buffer.Array);
    buffer = new ArraySegment<byte>(bufferedBytes, messageOffset, compressedBytes.Length);
   
    return buffer;
}

And during receiving, I need to decompress and then do decryption to get the original message:

C#
//Helper method to decompress an array of bytes
static ArraySegment<byte> DecompressBuffer(ArraySegment<byte> buffer, 
        BufferManager bufferManager, CompressionAlgorithm compressionAlgorithm)
{
    var memoryStream = new MemoryStream(buffer.Array, buffer.Offset, buffer.Count);
    var decompressedStream = new MemoryStream();
    var totalRead = 0;
    const int blockSize = 1024;
    byte[] tempBuffer = bufferManager.TakeBuffer(blockSize);
    using (Stream compressedStream = compressionAlgorithm == CompressionAlgorithm.GZip ?
        (Stream)new GZipStream(memoryStream, CompressionMode.Decompress) :
        (Stream)new DeflateStream(memoryStream, CompressionMode.Decompress))
    {
        while (true)
        {
            int bytesRead = compressedStream.Read(tempBuffer, 0, blockSize);
            if (bytesRead == 0)
                break;
            decompressedStream.Write(tempBuffer, 0, bytesRead);
            totalRead += bytesRead;
        }
    }
    bufferManager.ReturnBuffer(tempBuffer);

    byte[] decompressedBytes = decompressedStream.ToArray();
    byte[] bufferManagerBuffer = bufferManager.TakeBuffer
                                 (decompressedBytes.Length + buffer.Offset);
    Array.Copy(buffer.Array, 0, bufferManagerBuffer, 0, buffer.Offset);
    Array.Copy(decompressedBytes, 0, bufferManagerBuffer, 
               buffer.Offset, decompressedBytes.Length);

    buffer = new ArraySegment<byte>(bufferManagerBuffer, 
                                    buffer.Offset, decompressedBytes.Length);
    buffer = Cryptographer.DecryptBuffer(buffer, bufferManager);
    bufferManager.ReturnBuffer(buffer.Array);

    return buffer;
}

Cryptographer class is responsible to encrypt and decrypt data here. I keep all the Gzip and Encryption stuff in GlobalCommonLib assembly and here is the look of defining custom binding in my client side configuration file:

Image 5

Here, I have highlighted the portion of the custom binding definition. I have used httpTransport to enable sending/receiving data over HTTP which has been used in between Client and Routing service here.

In Routing service configuration File, use the same configuration there. Take a look into the service endpoint definition:

XML
<services>
    <service behaviorConfiguration="RoutingBehavior" 
     name="System.ServiceModel.Routing.RoutingService">
        <endpoint binding="customBinding" 
           bindingConfiguration="secureCustomBinding"
           name="RoutingEndpoint" 
           contract="System.ServiceModel.Routing.IRequestReplyRouter" />
        <endpoint address="mex"
                  binding="mexHttpBinding"
                  contract="IMetadataExchange" />
    </service>
</services>

And here is binding definition of Routing service end:

XML
<extensions>
  <bindingElementExtensions>
    <add name="compression" 
     type="GlobalCommonLib.GZipMessageEncodingElement, GlobalCommonLib"/>
  </bindingElementExtensions>
</extensions>
<bindings>
   <customBinding>
    <binding name="secureCustomBinding" receiveTimeout="infinite" sendTimeout="01:00:00">
      <compression innerMessageEncoding="textMessageEncoding" compressionAlgorithm="GZip"/>
      <httpTransport/>
    </binding>
  </customBinding>
</bindings>

Use Regular Security in Binding While Forwarding Message to Back End Service From Routing Service

In the Routing service, I have defined the routing filter based on the ServiceContract that Client used to call the service. For this reason, I have defined a custom Routing filter there.

XML
<client>
  <endpoint name="ServiceA"
            address="net.tcp://localhost:9092/ServiceA"
            binding="netTcpBinding" bindingConfiguration="securityNetBinding"
            contract="*"/>
  <endpoint name="ServiceB"
            address="net.tcp://localhost:9093/ServiceB"
            binding="netTcpBinding" bindingConfiguration="securityNetBinding"
            contract="*"/>
</client>
<routing>
  <filters>
    <filter name="RegisterServiceAFilter" filterData="IServiceA" 
       customType="RoutingServicePOC.Filter.ActionMessageFilter, RoutingServicePOC"  
       filterType="Custom" />
    <filter name="RegisterServiceBFilter" filterData="IServiceB" 
       customType="RoutingServicePOC.Filter.ActionMessageFilter, RoutingServicePOC" 
       filterType="Custom" />        
  </filters>
  <filterTables>
    <filterTable name="CentralRoutingTable">
      <add filterName="RegisterServiceAFilter" endpointName="ServiceA" priority="0"/>
      <add filterName="RegisterServiceBFilter" endpointName="ServiceB" priority="0"/>
    </filterTable>
  </filterTables>
</routing>
<behaviors>
      <serviceBehaviors>
    <behavior name="RoutingBehavior">
      <routing routeOnHeadersOnly="true" filterTableName="CentralRoutingTable" />
      <serviceMetadata httpGetEnabled="true" />
      <serviceDebug includeExceptionDetailInFaults="true" />
    </behavior>
  </serviceBehaviors>
</behaviors>

And I have configured a netTcp binding to use in between Routing-Service and Back-end services.

XML
<netTcpBinding>
<binding name="securityNetBinding" receiveTimeout="00:10:00" 
 sendTimeout="00:10:00" maxReceivedMessageSize="2147483647">
  <readerQuotas maxDepth="200" maxStringContentLength="2147483647" maxArrayLength="16384"
    maxBytesPerRead="4096" maxNameTableCharCount="16384" />
  <security mode="Transport">
    <transport protectionLevel="EncryptAndSign" />
  </security>
</binding>
</netTcpBinding>

Create an Authentication Manager to Do Authentication by User Name and Password to Avoid Anonymous Calls

I have defined a Custom Authentication Manager in ServiceCommonLib which has been used in both backend Services - Service A and Service B.

C#
public class CentralUserIdentity : IIdentity
{
    private readonly CentralRequestSession _session;
    public CentralUserIdentity(CentralRequestSession session)
    {
        this._session = session;
    }

    public string Name
    {
        get { return _session.UserName; }
    }

    public string AuthenticationType
    {
        get { return "Central"; }
    }

    public bool IsAuthenticated
    {
        get { return true; }
    }

    public CentralRequestSession Session
    {
        get { return _session; }
    }
}

public class CentralPrincipal : IPrincipal
{
    readonly IIdentity _identity;
    string[] _roles = null;

    public CentralPrincipal(IIdentity identity)
    {
        this._identity = identity;
    }

    public static CentralPrincipal Current
    {
        get
        {
            return Thread.CurrentPrincipal as CentralPrincipal;
        }
    }

    public IIdentity Identity
    {
        get { return _identity; }
    }

    public string[] Roles
    {
        get
        {
            //Findout Role and set here 
            return _roles;
        }
    }

    public bool IsInRole(string role)
    {
        //Findout Role and set here 
        return true;
    }       
}

public class CentralAuthenticationManager : ServiceAuthenticationManager
{
    public override ReadOnlyCollection<IAuthorizationPolicy> Authenticate(
       ReadOnlyCollection<IAuthorizationPolicy> authPolicy, 
                          Uri listenUri, ref Message message)
    {
        var session = CredentialHelper.GetSessionData(message);
        CheckCredentials(session);

        var identity = new CentralUserIdentity(session);
        IPrincipal user = new CentralPrincipal(identity);
        message.Properties["Principal"] = user;

        return authPolicy;
    }

    public void CheckCredentials(CentralRequestSession credentials)
    {
        System.Console.WriteLine
               ("Checking Credentials for {0}..........", credentials.UserName);
        // check the user and password against a database; 
        // if not match 
        // throw new AuthenticationException("Incorrect credentials!");        
        System.Console.WriteLine( "{0} is Valid!!", credentials.UserName);
    }
}

For this, I have extended the ServiceAuthenticationManager and you need to configure this custom authentication manager in your configuration file. In the Authenticate method, I have retrieved the headers of username and password which I have sent earlier while sending request to routing service. Router has forwarded that message to the backend service and here in backend service, I can retrieve those stuff and do authentication against any database.

C#
var session = CredentialHelper.GetSessionData(message);
CheckCredentials(session);

In Check Credential, you can do your validation and can do it against database if you want. Let's take a look into the configuration file:

Click to enlarge image

Here, I have added my custom authentication manager into the service behavior configuration section that has been used as Service behavior in the backend service.

OK, that is all. Thanks guys for reading. Have a good day!!

References

History

  • 13th September, 2012: Initial version

License

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