I have a scenario where I would like to be able to shield a large number of clients against redeployment and relocation of a large number of services.
I also have the requirement that services must do authentication and authorization based on the identity and role.
A solution to this would be to put a router which does the authentication and route messages to the correct service on the backend. The service can then do its own authorization based on role(s) the identity belongs to.
The assumption here is that clients supports the WS-* specifications (ws-security, ws-secureconvertion), in other words the client would have .NET 3.0 installed and can support the wsHttpBinding.
The router will be put in a DMZ, each service at the backend trusts the router and that the router did the authentication for them.
The authentication is done with the MemberShipProvider, which means the router is configured to use the MemberShipProvider :
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="MembershipProvider" membershipProviderName="SqlMembershipProvider"/>
The binding for the router uses message authentication and is configured as such:
<bindings>
<wsHttpBinding>
<binding name="secure">
<security mode="Message">
<message clientCredentialType="UserName"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
The client must present a user/password which is valid in the context of the MemberShipProvider, so we are not using the Windows account to authenticate the client here, although its possible and perfectly valid, but the idea of maintaining > 100.000 users is not in our scope here, ask Amazon.
Each backend service is configured with this custom binding here:
<customBinding>
<binding name="aBackendService">
<security authenticationMode="UserNameOverTransport"/>
<windowsStreamSecurity/>
<tcpTransport/>
</binding>
</customBinding>
This means the router must create this custom binding when contacting the backend services. Creating this kind of custom binding is easy:
public static Binding CreateCustomBinding()
{
BindingElementCollection bindingElements = new BindingElementCollection();
bindingElements.Add(SecurityBindingElement.CreateUserNameOverTransportBindingElement());
WindowsStreamSecurityBindingElement wbe = new WindowsStreamSecurityBindingElement();
wbe.ProtectionLevel = System.Net.Security.ProtectionLevel.EncryptAndSign;
bindingElements.Add(wbe);
bindingElements.Add(new TcpTransportBindingElement());
CustomBinding backendServiceBinding = new CustomBinding(bindingElements);
return backendServiceBinding;
}
Now the authenticationMode="UserNameOverTransport" takes care of flowing the identity of the caller and the <tcpTransport/> and <windowsStreamSecurity/>
handles the authentication of the router and does message protection, here EncryptAndSign is the default.
Each backend service does the authorization with a custom ServiceAuthorizationManager and is configured like this:
<!-- Configure role based authorization to use the Role Provider -->
<serviceAuthorization principalPermissionMode="UseAspNetRoles" roleProviderName="SqlRoleProvider" serviceAuthorizationManagerType="ServiceAuthMgr.MyServiceAuthorizationManager, ServiceAuthMgr">
The default authentication mode on the backend service is Windows, so this means the identity will be mapped to a Windows account. This will not work here as our router have already authenticated the identity using the MemberShipProvider, so we need to "disable" this default behaviour by configuring our own UserNamePasswordValidator which in our case does nothing!
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="aService.MyUserNamePasswordValidator, aServiceHost"/>
okay we are all set now and we start by defining the routers service contract
[ServiceContract()]
public interface IRouter
{
[OperationContract(Action="*",ReplyAction="*")]
Message ProcessMessage(Message message);
}
This operation contract has the notation of what is called an unmatched message handler. The basic idea is that each EndpointDispatcher signals the ChannelDispatcher that it can handle any messages. This is indicated by applying a behaviour with the AddressFilterMode set to Any as show here.
[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any, ValidateMustUnderstand = false)]
public class RouterImpl : IRouter
{
By default the EndpointDispatcher will try to invoke methods that corresponds to the Actions on a service contract, which when exported to wsdl is seen as the SOAPAction.
Each EndpointDispatcher maintains a collection of actions that can be invoked and in our case we indicate that we can handle any action and any ReplyAction, so the ChannelDispatcher will go ahead and send us the message.
Lets look at the ProcessMessage implementation, which is the core piece of code that makes the routing works.
public Message ProcessMessage(Message message)
{
Message replyMsg = null;
switch (message.Headers.To.AbsoluteUri)
{
case "urn:MyServer":
#endregion
//lookup urn:MyServer in cache/database and get the epa.
//epa = new EndpointAddress(new Uri("http://......"))
´ ChannelFactory<IRouter> dest = new ChannelFactory<IRouter>(custBinding, epa);
dest.Credentials.UserName.UserName = ServiceSecurityContext.Current.PrimaryIdentity.Name;
Message strippedMsg = Message.CreateMessage(MessageVersion.Default, message.Headers.Action, message.GetReaderAtBodyContents());
IRouter router = dest.CreateChannel(epa);
Message msg = router.ProcessMessage(strippedMsg);
replyMsg = Message.CreateMessage(MessageVersion.Default, msg.Headers.Action, msg.GetReaderAtBodyContents());
dest.Close();
break;
}
return replyMsg;
}
}
The first thing that might seem strange is I create a ChannelFactory of IRouter which takes the custom binding and the endpoint of the located backend service.
Then I pass in the Identity of the client which can be found in the ServiceSecurityContext.Current.PrimaryIdentity.Name. I then take the body of the message and creates a new message, then create the channel and sends the message using my own ProcessMessage operation!, then I strip the headers off the reply message and creates a new message with the body and return that message to the caller.
Okay what I do here is I contact the backend service using the correct EndpointAddress (which was found using the logical uri of the client) and when using the ProcessMessage to send the incoming message, the ChannelDispacther on the backend service will see if the EndpointDispatcher can handle the message given the SOAPAction that can be found in the messages. The backend service will perform its work, just as if it was invoked directly by the client using the direct operation contract.
The reason why I strip off the headers on the request/reply message is due to the fact that we are not using the same binding and I can not simply piggyback the message on the a new call to the backend service. This will produce duplicate and wrong headers according the WS-*, so cutting off the headers will produce a correct message according the our backend service binding. The reply message will be correct also when it returns to the client, WCF will add what ever headers there must be, according to the WS-*.
Now to the client side:
A client will prior to all this have either been handed a service contract or would have generated a proxy to the backend service. If we have generated a proxy given the backend mex, the client side configuration would look like this:
<endpoint address="urn:MyServer"
behaviorConfiguration="ClientBehavior"
binding="wsHttpBinding"
bindingConfiguration="WSHttpBinding_IaService"
contract="WebUIClient.localhost.IaService"
name="WSHttpBinding_IaService" >
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_IaService">
<security mode="Message">
<message clientCredentialType="UserName" />
</security>
</binding>
</wsHttpBinding>
</bindings>
Notice the address which points to a logical address!. This is the logical Uri that is passed in from the client and this is the uri that the router will eventually use to fetch the real EndpointAddress (from some storage) of the backend service. This uri is configured by hand.
Okay so when the client contacts our backend service is creates an EndPointAddress that points to our router location:
Uri via = new Uri("http://localhost:8080/Router");
Then the client creates the logical endpoint like this:
EndpointAddress endptadr = new EndpointAddress(new Uri("urn:MyServer"));
It then creates a channel, passing in the logical endpoint and the Via uri is then set to our router.
So go contact this logical endpoint Via the routers endpoint, is what is says here.
ChannelFactory<IaService> factory = new ChannelFactory<IaService>("WSHttpBinding_IaService");
We pass in our credentials according to the binding.
factory.Credentials.UserName.UserName = "Alice";
factory.Credentials.UserName.Password = "abc!123";
we then create the channel
IaService client = factory.CreateChannel(endptadr, via);
and invoke an operation.
OrderMessage msg = new OrderMessage();
OrderMessageReply res = client.CreateOrder(msg);
The backend service will receive this request via the router but with the initial Identity, which the backend can perform authorization on. In my case I am building up some other claims by implementing the IAuthorizationPolicy and then have the ServiceAuthorizationManager perform the GO no GO based on the built claims and roles.
Source code available upon request.
"If I had more time, I would have written a shorter letter."
-- Pascal.
12:22:38 PM
|