Archives

Thursday, June 9, 2011

WCF Pattern: Secure Dynamic Service Endpoints

Problem

You require features not available through the current binding implemented by a service. For security purposes, it is not appropriate to implement multiple static endpoints for the same service.

WCF bindings support a range of features, but not all bindings implement all features. Different operations in a service might require specific features that are mutually incompatible or that are not appropriate for all operations. WCF does not allow you to modify the bindings for a service without stopping and restarting the service. In these circumstances, it is common for a service to expose multiple endpoints. Each endpoint can implement a different binding, and the client can connect to the most appropriate endpoint to perform an operation. If the service requires the client to be authenticated, then it must authenticate the connection from the client to each endpoint. Not all authentication mechanisms are available over every type of binding, so for this approach to be successful, the service must limit the functionality to that available for the authentication mechanism.

Forces

  • You want to implement functionality that requires different binding configurations.
  • You do not want to stop and restart the service to modify the binding configurations.
  • You need to ensure that only authenticated clients can invoke the operations exposed by the service.
  • You do not want to limit the functionality of the service based on the authentication mechanism used by the service.

Example Scenario

A WCF service implements basic authentication and transport security by using the BasicHttBinding binding. For performance reasons, one operation named UploadData exposed by the service expects a streamed request. However, streamed requests are not supported when using Basic, Digest, or NTLM authentication over transport security. You need to build a service that provides basic authentication, but that also exposes an endpoint that supports request streaming.

Solution

Add an authenticated operation to the service that creates a new service instance with an endpoint configured to meet the required functionality. Generate a unique, private address for the endpoint and return this address to the client. The client uses this address to connect to the new service instance and invoke operations. The service instance is closed when the operations have completed.



Security Note: This approach depends on the uniqueness of the address generated for the service instance to protect the operation. The client application can only obtain this address by invoking an authenticated operation. The service instance must be short-lived to minimize the possibility of an unauthenticated client application being able to successfully “guess” the address of the service instance.

Implementation

The sample implementation shown in this section comprises three elements:
  • The authenticated service. This is the principal service that implements the authenticated business operations for your system. Additionally, this service implements the IQueryStreamService interface. This interface is an element of the Secure Dynamic Service Endpoints pattern.
  • The streaming service. This is the service that implements the streaming operations. The authenticated service constructs and configures the host for this service dynamically.
  • The client application. This application connects to the authenticated service and invokes the GetStreamServiceAddress operation to create an instance of the streaming service. The client application uses the address returned by this operation to open a channel to the streaming service and invoke the UploadData streaming operation.

Authenticated Service

The service providing the authenticated operations additionally implements the IQueryStreamService interface shown below. This interface exposes the GetStreamServiceAddress method that returns a string containing the address of a new service instance that implements a binding that supports streaming.
[DataContract(Namespace = "http://contentmaster.com")]
public class QueryStreamServiceFault
{
[DataMember]
public string Message;

[DataMember]
public string SourceMethod;
}

[ServiceContract(Namespace = "http://contentmaster.com")]
public interface IQueryStreamService
{
[FaultContract(typeof(QueryStreamServiceFault))]
[OperationContract]
string GetStreamServiceAddress();
}

The QueryStreamService shown in the next code example implements the IQueryStreamService interface. It performs the following tasks:
  • It obtains a reference to the ServiceHost object hosting the service. The method requires the address of the current service to use as a basis for the address of the streaming service.
  • It instantiates a new ServiceHost object for the streaming service.
  • It obtains a reference to the IStreamService service contract (described later). The streaming service must implement this contract.
  • It creates a BasicHttpBinding binding and configures it to support request streaming.
  • It constructs an address for the streaming service by appending a random GUID to the end of the base address of the current service (It also inserts the text “/Streamer/” into the address—this is optional).
  • It creates a ServiceEndpoint for the streaming service based on the IStreamService contract, the binding configured to support streaming, and the constructed address.
  • It starts a new instance of the streaming service and returns the address of this instance to the client.
public class QueryStreamService : IQueryStreamService
{
public string GetStreamServiceAddress()
{
try
{
// Get the details of the host environment
// from the current operation context
ServiceHost host = OperationContext.Current.Host as ServiceHost;

// Create a new service host to host the service instance
ServiceHost newHost = new ServiceHost(typeof(StreamService));

// Build a contract description for the IStreamService contract
ContractDescription contract = new
ContractDescription("IStreamService", "http://contentmaster.com");

// Assume that the binding to be used is BasicHttpBinding
// Streaming does not work with ws* bindings
// Note: Can get current binding for service by using:
// Binding binding = host.Description.Endpoints[0].Binding;

BasicHttpBinding binding = new BasicHttpBinding();

// Enable streamed requests for this binding
binding.TransferMode = TransferMode.StreamedRequest;
binding.MaxReceivedMessageSize = Int64.MaxValue;

// Construct an address using the base address of the service
// and a random GUID string
string privateEndpointSuffix = Guid.NewGuid().ToString();
string address = String.Format("{0}/Streamer/{1}",
host.BaseAddresses[0], privateEndpointSuffix);

// Create a new private endpoint for the IStreamService contract
// and add the endpoint to the new service host
ServiceEndpoint endpoint = new
ServiceEndpoint(contract, binding, new EndpointAddress(address));
newHost.AddServiceEndpoint(typeof(IStreamService), binding, address);

// Start the new service host instance
newHost.Open();

// Return the address of the new service instance to the client
return address;
}
catch (Exception e)
{
QueryStreamServiceFault qssf = new QueryStreamServiceFault();
qssf.Message = e.Message;
qssf.SourceMethod = "GetStreamServiceAddress";
throw new FaultException<QueryStreamServiceFault>(qssf);
}
}
}

Streaming Service

The streaming service in this example implements the IStreamService interface shown below. The UploadData operation enables a client application to send data to the service as a streamed request. The CloseHost method closes the streaming service. The client application calls CloseHost when it has completed sending any streamed requests to the streaming service.
Note: The CloseHost method is marked as a one-way operation because it can never return.
[ServiceContract(Namespace="http://contentmaster.com")]
public interface IStreamService
{
[OperationContract]
void UploadData(Stream data);

[OperationContract(IsOneWay=true)]
void CloseHost();
}

The following code shows an example implementation of the streaming service. In this example, the streamed data is assumed to be a bitmap which is saved to a “.bmp” file. Replace this code with your own business logic, and handle exceptions as necessary.
Note: The CloseHost method silently fails if an exception occurs. It cannot return an exception information to the client as it is a one-way operation, and in the event of a failure in this method the host is most likely already in a Closed or Closing state and will be recycled. If necessary, you could add code to log the exception to the Windows Application Event log.
public class StreamService : IStreamService
{
public void UploadData(Stream data)
{
try
{
// Stream the data into the service
System.Drawing.Bitmap bm = new System.Drawing.Bitmap(data);
bm.Save(@"E:\StreamedData.bmp");
}
catch (Exception e)
{
// Handle exceptions
}
}

public void CloseHost(ServiceHost host)
{
try
{
// Tidy up - close the host
if (host.State != CommunicationState.Closing &&
host.State != CommunicationState.Closed)
host.Close();
}
catch
{
// Silently fail on exception — the host will be closed
}
}
}

Client Application

The code below shows a sample client application. The code performs the following tasks:
  • It creates a proxy object for communicating with the authenticated service that implements the IQueryStreamService service contract. In this example, the proxy class was generated by using the Add Service Reference functionality of Visual Studio.
  • It invokes the GetStreamService operation through the proxy object to create a new instance of the streaming service and obtain the address of this service.
  • It creates a BasicHttpBinding binding for communicating with the streaming service.
  • It creates a ChannelFactory object for creating an instance of a channel that can communicate with the streaming service, based on the binding and the address retrieved by the GetStreamService operation.
  • It instantiates a proxy for communicating with the streaming service by using the ChannelFactory object.
  • It opens a stream of data and invokes the UploadData operation in the streaming service passing it this stream. In this example, the streamed data is the contents of a “.bmp” file.
  • It calls the CloseHost operation to close the streaming service.
  • It closes the channel and finishes.
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using StreamServiceClient.QueryStreamService;
using System.IO;

namespace StreamServiceClient
{
class Program
{
static void Main(string[] args)
{
try
{
QueryStreamServiceClient proxy = new
QueryStreamServiceClient("BasicHttpBinding_IQueryStreamService");
string address = proxy.GetStreamServiceAddress();
Console.WriteLine("Address is {0}", address);

Binding binding = new BasicHttpBinding();
Console.WriteLine("Binding is {0}", binding.Name);

ChannelFactory<IStreamService> channelFactory =
new ChannelFactory<IStreamService>(binding, address);
IStreamService streamServiceProxy =
channelFactory.CreateChannel();

FileStream fs =
new FileStream(@"E:\Test.bmp",FileMode.Open,FileAccess.Read);
streamServiceProxy.UploadData(fs);
streamServiceProxy.CloseHost();
channelFactory.Close();
Console.WriteLine("Finished");
}
catch (FaultException<QueryStreamServiceFault> fex)
{
Console.WriteLine("Fault exception thrown in {0}: {1}",
fex.Detail.SourceMethod, fex.Detail.Message);
}

catch (Exception e)
{
Console.WriteLine("Fault exception thrown: {0}", e.Message);
}
}
}
}

No comments:

Post a Comment