Forms Auth & Federated Security (part 2)
Here I talked about an approach that you can use to integrate your legacyJ forms auth applications with STS and bring them into the world of federated security.
One major hurdle in implementing this approach is to flow Forms Auth cookie to the STS so that it can authenticate the caller and can issue a token. With wsFederationHttpBinding, you don’t directly talk to the STS rather federation binding talks to the STS as part of your service call. After wsFederationHttpBinding successfully acquired a token, only then your service is called and token is sent as part of the call. This is good because it hides all the token acquisition/forwarding complexity from you and offers you a simple programming model.
Now in our case, we need to intercept the messages sent by FedBinding to the STS so that we can send our Forms Auth cookie along with the message.
At this point a very brief diagrammatic overview of WCF message security framework will help:
On the client side TokenProvider is responsible for providing tokens to message security layer. There is TokenProvider for each type of type (Usernname, IssuedToken etc).
IssuedSecurityTokenProvider is used when a SAML token is required by the message security layer and this is the guy we need to intercept.
Let’s start by creating a custom ClientCredentials:
public class ClientCredentialsWrapper : ClientCredentials
{
public ClientCredentialsWrapper()
{}
public ClientCredentialsWrapper(ClientCredentials other):base(other)
{}
public override SecurityTokenManager CreateSecurityTokenManager()
{
return new ClientSecurityTokenManagerWrapper(this);
}
protected override ClientCredentials CloneCore()
{
return new ClientCredentialsWrapper(this);
}
}
Next our custom SecurityTokenManager:
class ClientSecurityTokenManagerWrapper : ClientCredentialsSecurityTokenManager
{
public ClientSecurityTokenManagerWrapper(ClientCredentials parent)
: base(parent)
{ }
public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement)
{
var provider = base.CreateSecurityTokenProvider(tokenRequirement);
var issuedProvider = provider as IssuedSecurityTokenProvider;
if (issuedProvider != null)
issuedProvider.IssuerChannelBehaviors.Add(new MessageInspectorInstallerBehavior());
return provider;
}
}
Here I have added endpoint behaviour in the ChannelFactory used by IssuedSecurityTokenProvider to talk to STS.
class MessageInspectorInstallerBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{ }
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(new CookieFlowMessageInspector());
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{ }
public void Validate(ServiceEndpoint endpoint)
{ }
}
Here I’m simply installing a MessageInspector which looks like this:
class CookieFlowMessageInspector : IClientMessageInspector
{
public void AfterReceiveReply(ref Message reply, object correlationState)
{ }
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
object prop = null;
HttpRequestMessageProperty rmp = null;
if (request.Properties.TryGetValue(HttpRequestMessageProperty.Name, out prop))
rmp = prop as HttpRequestMessageProperty;
else
rmp = new HttpRequestMessageProperty();
rmp.Headers[HttpRequestHeader.Cookie] = FormAuthUtility.GetCookieHeaderFromRequest();
request.Properties[HttpRequestMessageProperty.Name] = rmp;
return null;
}
}
Client code will stay the same and you just have to call just one extension method to hook everything together.
var proxy = new STSCookieServiceReference.EchoServiceClient();
var certPath = HttpContext.Current.Server.MapPath("~/localhost.cer");
proxy.ClientCredentials.ServiceCertificate.DefaultCertificate = new X509Certificate2(certPath);
proxy.EnableIssuedTokenProviderCookieFlow();
proxy.Echo("Welcome.");
And this is how extension method is implemented.
public static void EnableIssuedTokenProviderCookieFlow<TChannel>(this ClientBase<TChannel> source) where TChannel : class
{
var orignal = source.Endpoint.Behaviors.Remove<ClientCredentials>();
source.Endpoint.Behaviors.Add(new ClientCredentialsWrapper(orignal));
}
I have attached complete solution with this post. Feel free to download and have a look.
Download: FormsAuthFedSecurity.zip