Interactive IdP With SAML-P WIF Extension

by Jimmy 16. August 2011 11:25

A while ago Microsoft released the SAML-P Extension for the Windows Identity Foundation which is a great and long desired addition to the framework. As it happens, I’m working on an identity platform here in Symantec, and our server has to support the SAML 2 protocol. Imagine my excitement when I’ve seen the announcement and I thought that we can finally discard our custom implementation. Also, imagine my disappointment when I’ve seen that the sample IdP is returning a token without any interaction. Moreover, the way the extension was created makes any user interaction during the process impossible without hacking. By “an interaction” I mean the situation when a user is redirected to the IdP, where he has to log in and only then the token is issued and send back. Just like in any example using WS-Federation you can find in the training kit or the SDK. I have no idea why Microsoft did it this way but I’m sure it’s just a temporary measure and the final version will have all required extension points. Don’t disappoint me guys! Nevertheless I decided I’m not going to wait until that will be solved and crack the thing open myself.

So what do we have out of the box? If you look at the Getting Started sample from the extension download, you will see that there is the IdP project that has LaxSingleSignOnService (inherited from Saml2Service). This service is created in the constructor of the Saml2AuthenticationModuleWithIdentityProvider (inheriting from Saml2AuthenticationModule) that in turn is declared in the web.config. The first step was to understand how cogwheels are turning here. I fired up Reflector and started digging. Here is what I found.

  • Saml2AuthenticationModule constructor loads configuration metadata and creates the whole bunch of classes such as token resolvers, serialisers and so on. It also creates the assertion consumer and single logout services. The Saml2AuthenticationModuleWithIdentityProvider constructor adds LaxSingleSignOnService to that stack.
  • The OnAuthenticateRequest method is checking content of the request, finding a service that can process the content and then calling the Process method on that service.
  • The process method of the Saml2Service is just calling the abstract ProcessCore that is overloaded in custom LaxSingleSignOnService.
  • The ProcessCore method of the LaxSingleSignOnService creates a token and a response and sends it back.

Knowing that was fairly easy to see where I had to break in to get an interaction. I had to get rid of the Saml2AuthenticationModule and replace it’s logic in the application in controlled manner. The perquisite was to create the metadata configuration. The processing itself had to be broken when the request message is received. That is the moment when a user should be authenticated and then processing should continue. Simple, isn’t it?

Let’s look at the implementation then. I’m not going to put a full solution here as it contains a lot of custom code but snippets below should give you enough information to hack your own solution.

1. Create metadata configuration.

The first step is to replicate the Saml2AuthenticationModule constructor and create all required configuration classes, ideally once, when application is started, and then keep the result somewhere for later use. My IdP is an MVC3 application so put that in the global.asax.cs and stored it in the Application for later use. There is no need to do that every time.

You can also see that I’m not using the metadata file here but instead I’m creating the MetadataEntityConfiguration manually. The reason for that was that I had full IdP configuration already in place and didn’t want to duplicate it in the metadata file. If you can use the file approach just look in the reflector and get the code from there.

public static void RegisterSaml2Metadata()
{
  var entityConfiguration = new MetadataEntityConfiguration(SamlFederatedAuthentication.Configuration.TrustedPartners);
  SecurityTokenResolver signatureTokenResolver = new EntitySecurityTokenResolver(entityConfiguration, 
    FederatedAuthentication.ServiceConfiguration.IssuerTokenResolver);
  SecurityTokenResolver serviceTokenResolver = FederatedAuthentication.ServiceConfiguration.ServiceTokenResolver;
  var serializer = new Saml2ProtocolSerializer(signatureTokenResolver, serviceTokenResolver);
  var redirectSerializer = new HttpRedirectBindingSerializer(serializer, signatureTokenResolver);
  var postSerializer = new HttpPostBindingSerializer(serializer);
  BoundedCache<RequestTrackingData> requestStateStorage = new BoundedCache<RequestTrackingData>(0x7fffffff, TimeSpan.FromMinutes(10.0));
  redirectSerializer.RequestStateStorage = requestStateStorage;
  postSerializer.RequestStateStorage = requestStateStorage;
  var bindingSerializers = new BindingSerializerCollection();
  bindingSerializers.SetSerializer(ProtocolBindings.HttpRedirect, redirectSerializer);
  bindingSerializers.SetSerializer(ProtocolBindings.HttpPost, postSerializer);
  MessageValidator messageValidator = new MessageValidator();
  var services = new ServiceCollection();
  services.SetService(ServiceType.SingleSignOn, new SamlSingleSignOnService(entityConfiguration));
  services.SetService(ServiceType.SingleLogout, new SamlSingleLogoutService(entityConfiguration, messageValidator));
  HttpContext.Current.Application["BindingSerializers"] = bindingSerializers;
  HttpContext.Current.Application["Services"] = services;
}

2. Receive an incoming message

The first step in the processing is to receive the incoming message and find a service within the configuration created before. The request is coming to an action of my controller but the OnAuthorization filter kicks in before the action is executed. I’m extracting the message from the request, finding the service that can process it and then both are stored in the TempData. You need to be careful here with the url casing as GetServices method is case sensitive. I decided to normalize the url to lowercase and this is what the NormalizeUrl method is doing.

protected override void OnAuthorization(AuthorizationContext filterContext)
{
  BindingSerializerCollection samlBindingSerializers = HttpContext.Application["BindingSerializers"] 
    as BindingSerializerCollection;
  ServiceCollection samlServices = ValueProvider.GetValue("samlServices").RawValue as ServiceCollection;

  HttpMessage httpMessage = null;
  HttpBindingSerializer serializer = FindSerializer(SamlFederatedAuthentication.Configuration.ServiceEndpointConfiguration, 
    samlBindingSerializers, out httpMessage);
  if (serializer != null && httpMessage != null)
  {
    MessageContainer messageContainer = serializer.Deserialize(httpMessage);
    Saml2Service service = FindService(SamlFederatedAuthentication.Configuration.ServiceEndpointConfiguration, samlServices, 
      messageContainer, httpMessage.ProtocolBinding);
    filterContext.Controller.TempData["messageContainer"] = messageContainer;
    filterContext.Controller.TempData["service"] = service;
  }
  base.OnAuthorization(filterContext);
}
private HttpBindingSerializer FindSerializer(EndpointConfiguration endpointConfiguration, 
  BindingSerializerCollection samlBindingSerializers, out HttpMessage httpMessage)
{
  ReadOnlyCollection<Uri> bindings = endpointConfiguration.GetBindings(NormalizeUrlCase(Request.Url));
  for (int i = 0; i < bindings.Count; i++)
  {
    HttpBindingSerializer serializer = samlBindingSerializers.GetSerializer(bindings[i]);
    if (serializer.TryReadHttpMessage(Request, out httpMessage))
    {
      return serializer;
    }
  }
  httpMessage = null;
  return null;
}
private Saml2Service FindService(EndpointConfiguration endpointConfiguration, ServiceCollection samlServices, 
  MessageContainer messageContainer, Uri protocolBinding)
{
  ReadOnlyCollection<ServiceType> services = endpointConfiguration.GetServices(NormalizeUrlCase(Request.Url), protocolBinding, false);
  if (services.Count == 0)
  {
    throw new InvalidOperationException(string.Format(Properties.Resources.ID4505, Request.Url, protocolBinding));
  }
  for (int j = 0; j < services.Count; j++)
  {
    Saml2Service service = samlServices.GetService(services[j]);
    if (service.CanProcess(messageContainer))
    {
      return service;
    }
  }
  throw new InvalidOperationException(string.Format(Properties.Resources.ID4506, NormalizeUrlCase(Request.Url), 
    protocolBinding, messageContainer.Message.GetType().FullName, messageContainer.Message.Id));
}

3. Authenticate the user

That doesn’t require any comments.

4. Create and send response

After authentication the user is redirected back to the action was originally called. This time OnAuthorization filter will do nothing as the request doesn’t contain the message anymore. The action itself gets the serialisers collection, the message and the service from their respective storage and then call Process method on the service. Result is packed in the HttpMessage and send back to the request originator.

public ActionResult SamlpPost()
{
  BindingSerializerCollection samlBindingSerializers = HttpContext.Application["BindingSerializers"] as BindingSerializerCollection;
  MessageContainer messageContainer = TempData["messageContainer"] as MessageContainer;
  Saml2Service service = TempData["service"] as Saml2Service;
  OutgoingMessageContainer outgoingMessage = service.Process(messageContainer);
  HttpBindingSerializer serializer = samlBindingSerializers.GetSerializer(outgoingMessage.Endpoint.Binding);
  HttpMessage httpMessage = serializer.Serialize(outgoingMessage);
  serializer.WriteHttpMessage(Response, httpMessage);
  ControllerContext.HttpContext.ApplicationInstance.CompleteRequest();
  return new EmptyResult();
}

Sounds not that bad and it was pretty straightforward and works nicely.

HTH

Tags: , , , , ,

Identity | SAML | Security | WIF

blog comments powered by Disqus