Flowing Identity from a Client to a Service when using RESTful WCF Part 2 - A Solution
September 1, 2008This post will describe a technique to transparently insert a custom HTTP header into all requests generated by a WCF REST proxy, as well as a technique to extract that HTTP header from the request at the service for the purpose of flowing the digital identity of an authenticated user from a REST client to a REST service. In this case, digital identity is represented by an authorization token, which is just a string. You can read a detailed description of the problem in Part 1 of this post. A complete sample project containing the full source code of the solution is on CodePlex.
The basic idea is to insert a custom action into the WCF message pipeline that will extract a security token representing the identity of the currently logged in user and add the value of that token (just a string) to the outgoing API call (an HTTP request) by way of a custom HTTP header. Once the request is received by the service, a custom action inserted into the receive pipeline checks for the custom header and, upon finding it, de-serializes the identity token and uses the claims contained within it to set an appropriate security context (in this case by setting Thread.CurrentPrincipal).
Setting a Custom HTTP Header - The Easy Way
On the client side, I am making the assumption that a user has been authenticated somehow, is associated with a set of claims, and that a representation of the user along with her claims can be found at Thread.CurrentPrincipal. In anticipation of using Microsoft’s new Zermatt Identity framework, which introduces IClaimsPrincipal and IClaimsIdentity, we implemented our own versions of these interfaces. They will eventually be replaced by the Zermatt types once we start integrating Zermatt into our application. IClaimsPrincipal maintains a reference to IClaimsIdentity, which has a property of type IList<IClaim> that represents all the authorization relevant attributes of a user. Please see this post for a discussion of claims based identity management. The interface IClaim is pretty much just a name value pair.
Adding a custom header to a request is trivial if you add the header in the same scope as the service call made on the proxy. You can just wrap the entire call in an OperationContextScope using statement, and use the Headers property of WebOperationContext.Current:
using (var factory = new WebChannelFactory<imyservice>())
{
IMyService proxy = factory.CreateChannel();
using (new OperationContextScope((IClientChannel) proxy))
{
WebOperationContext.Current.OutgoingRequest.Headers.Add('x-mycompany-auth',
'tokenString');
proxy.DoItToIt();
}
}
That technique works, and would probably be just fine if you only needed to add the authorization token to a few select calls. However, in our scenario, we want to add the token to *every* call, so wrapping each API call with all that scoping garbage isn’t going to cut it. It’s ugly and about as un-DRY as it gets.
Extending WCF - The Transparent Way
The solution, of course, is to take advantage of the WCF extensibility points available to both the client and the server. We want to choose spots in the messaging pipelines as close to the actual delivery/receipt of the message as possible.That would be the the Message Inspection phase on the client and the Operation Context Initialization phase on the server.
WCF can be made to jump through our hoops by following these steps:
On the client side we need to
1. Define the MessageInspector
2. Create a behavior to register the MessageInspector
3. Install the behavior on the channel factory.
On the service side we need to
4. Define a OperationContextInitializer
5. Create a behavior to register the extension
6. Apply the behavior to the service.
The end result is that Thread.CurrentPrincipal on the service side will always reference an IClaimsPrincipal with the same claims associated with the Thread.CurrentPrincipal on the service consumer, and we can test those claims to authorize access, as demonstrated in
7. Use.
1. The MessageInspector
public class ClientAuthMessageInspector : IClientMessageInspector
{
#region IClientMessageInspector Members
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
//Get the HttpRequestMessage property from the Message
var httpRequest =
request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
//Make sure we have a valid property, or create one
if (httpRequest == null)
{
httpRequest = new HttpRequestMessageProperty();
request.Properties.Add(HttpRequestMessageProperty.Name, httpRequest);
}
//Add your token to the header. Simple as that.
//In real life, we injected an ITokenProvider using an IoC,
//instead of using static methods directly, but that was
//overkill for a sample.
httpRequest.Headers.Add(AuthenticationHelper.AUTH_TOKEN_HEADER_NAME,
AuthenticationHelper.GetAuthTokenForCurrentUser());
return null;
}
public void AfterReceiveReply(ref Message reply, object correlationState)
{
//nop
}
#endregion
}
2. The Behavior
public class ClientAuthBehavior : IEndpointBehavior
{
private readonly ClientAuthMessageInspector _messageInspector;
public ClientAuthBehavior(ClientAuthMessageInspector messageInspector)
{
_messageInspector = messageInspector;
}
#region IEndpointBehavior Members
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
//Install the message inspector
clientRuntime.MessageInspectors.Add(_messageInspector);
}
//no-op method implementations elided for clarity.
//See the sample project for the complete
//code listing.
#endregion
}
3. Apply
private void InitializeChannelFactory()
{
_channelFactory = new WebChannelFactory<imonkeyshavingservice>("shavingService");
_channelFactory.Endpoint.Behaviors.Add(new ClientAuthBehavior());
}
4. On the Service Side…OperationContextInitializer
public class ClaimsAuthContextInitializer : ICallContextInitializer
{
#region ICallContextInitializer Members
public Object BeforeInvoke(InstanceContext instanceContext,
IClientChannel channel,
Message message)
{
DetectCurrentUser();
return null;
}
public void AfterInvoke(Object correlationState)
{
}
#endregion
private static void DetectCurrentUser()
{
if (WebOperationContext.Current == null)
throw new InvalidOperationException("Only HTTP web requests are supported for this version of the API");
string authToken = WebOperationContext.Current
.IncomingRequest
.Headers[AuthenticationHelper.AUTH_TOKEN_HEADER_NAME];
if (AuthenticationHelper.SetCurrentUserFromAuthToken(authToken) == false)
{
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Forbidden;
throw new UnauthorizedAccessException(
"No authentication token was found in the headers of the current request.");
}
}
}
5. The Behavior
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ClaimsAuthServiceBehavior : Attribute, IServiceBehavior
{
#region IServiceBehavior Members
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
}
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase,
Collection<serviceendpoint> endpoints,
BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
var cd = cdb as ChannelDispatcher;
if (cd != null)
{
foreach (EndpointDispatcher ed in cd.Endpoints)
{
foreach (var dispatchOperation in
ed.DispatchRuntime.Operations)
{
dispatchOperation.CallContextInitializers
.Add(new ClaimsAuthContextInitializer());
}
}
}
}
}
#endregion
6. Apply
static void Main(string[] args)
{
_host = new WebServiceHost(typeof(MonkeyShavingService),
new Uri("http://localhost:8000"));
_host.Description.Behaviors.Add(new ClaimsAuthServiceBehavior());
var binding = new WebHttpBinding();
_host.AddServiceEndpoint(typeof(IMonkeyShavingService), binding, "");
_host.Open();
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
7. Last but not least, Use (this is a sample service method that restricts access to certain users)
public Monkey PutMonkeyInShaver(Monkey monkey)
{
//Only authenticated users can add monkeys to the shaver
if (CurrentUser == null)
{
SetStatusCode(HttpStatusCode.Forbidden,
"Only authenticated users can put monkeys in the shaver.");
return null;
}
_monkeys.Add(monkey);
ShaveMonkey(monkey);
return monkey;
}
private IClaimsPrincipal CurrentUser
{
get { return Thread.CurrentPrincipal as IClaimsPrincipal; }
}
It works, but…
Our ultimate solution does not extend or take advantage of any of the claims based security features built into WCF. While it may be possible to do so, it wasn’t immediately obvious to us how to go about it, so the solution presented here handles authorization out of band from the built in WCF authorization mechanisms.
As you may have guessed, I am NOT a WCF expert. That means that the approach outlined here could very well be a cheap hack for a problem solvable in a much more elegant way. If by some chance you happen across this post and know that to be the case, I’d love to hear your thoughts on the subject
I wasn’t able to find any guidance during my own research that hinted at a more appropriate approach to solving this problem, but that certainly doesn’t mean one doesn’t exist.
About the Sample Application
The sample application can be downloaded here and includes a complete, end to end REST API implemented in WCF. It includes separate DLL’s to represent the Service, the Contracts, the Client, a Service Host and a Utility DLL that contains shared authentication and authorization code. To get the most from the sample you should run the Service Consumer project, a Windows Forms application that allows you to invoke various operations of the API while impersonating users with various roles. In addition to demonstrating a possible approach to flowing a claims-based security token through a REST API, the sample demonstrates a fully functional, RESTful WCF service, which may be useful to you in its own right.
Here is a screen shot of the sample Windows Forms service client:
What’s Next
In the future I will be enhancing the sample to demonstrate our approach to defining authorization policies that operate against a set of claims using our Simple Expression Evaluator project, currently on CodePlex. This technique will show how a simple but powerful and flexible claims-aware rules engine can easily be constructed using any expression evaluator and a set of claims.













Add New Comment
Viewing 6 Comments
Thanks. Your comment is awaiting approval by a moderator.
Do you already have an account? Log in and claim this comment.
Do you already have an account? Log in and claim this comment.
Do you already have an account? Log in and claim this comment.
Do you already have an account? Log in and claim this comment.
Do you already have an account? Log in and claim this comment.
Do you already have an account? Log in and claim this comment.
Do you already have an account? Log in and claim this comment.
Add New Comment
Trackbacks
(Trackback URL)