April 2010 - Posts

A time ago I discussed how a custom REST Starter kit interceptor could be used to parse a SAML token in the Http Authorization header and wrap that into a ClaimsPrincipal that the WCF services could use. The thing is that code was initially created for Geneva framework, so it got deprecated quickly. I recently needed that piece of code for one of projects where I am currently working on so I decided to update it for WIF. As this interceptor can be injected in any host for WCF REST services, also represents an excellent solution for integrating claim-based security into WCF Data Services (previously known as ADO.NET Data Services).

The interceptor basically expects a SAML token in the Authorization header. If a token is found, it is parsed and a new ClaimsPrincipal is initialized and injected in the WCF authorization context.

public class SamlAuthenticationInterceptor : RequestInterceptor
{
  SecurityTokenHandlerCollection handlers;

  public SamlAuthenticationInterceptor()
    : base(false)
  {
    this.handlers = FederatedAuthentication.ServiceConfiguration.SecurityTokenHandlers;
  }

  public override void ProcessRequest(ref RequestContext requestContext)
  {
    SecurityToken token = ExtractCredentials(requestContext.RequestMessage);

    if (token != null)
    {
      ClaimsIdentityCollection claims = handlers.ValidateToken(token);

      var principal = new ClaimsPrincipal(claims);
      InitializeSecurityContext(requestContext.RequestMessage, principal);
    }
    else
    {
      DenyAccess(ref requestContext);
    }
  }

  private void DenyAccess(ref RequestContext requestContext)
  {
    Message reply = Message.CreateMessage(MessageVersion.None, null);
    HttpResponseMessageProperty responseProperty = new HttpResponseMessageProperty() { StatusCode = HttpStatusCode.Unauthorized };

    responseProperty.Headers.Add("WWW-Authenticate",
          String.Format("Basic realm=\"{0}\"", ""));

    reply.Properties[HttpResponseMessageProperty.Name] = responseProperty;
    requestContext.Reply(reply);

    requestContext = null;
  }

  private SecurityToken ExtractCredentials(Message requestMessage)
  {
    HttpRequestMessageProperty request = (HttpRequestMessageProperty)  requestMessage.Properties[HttpRequestMessageProperty.Name];

    string authHeader = request.Headers["Authorization"];

    if (authHeader != null && authHeader.Contains("<saml"))
    {
      XmlTextReader xmlReader = new XmlTextReader(new StringReader(authHeader));

      var col = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
      SecurityToken token = col.ReadToken(xmlReader);
                                
      return token;
    }

    return null;
  }

  private void InitializeSecurityContext(Message request, IPrincipal principal)
  {
    List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>();
    policies.Add(new PrincipalAuthorizationPolicy(principal));
    ServiceSecurityContext securityContext = new ServiceSecurityContext(policies.AsReadOnly());

    if (request.Properties.Security != null)
    {
      request.Properties.Security.ServiceSecurityContext = securityContext;
    }
    else
    {
      request.Properties.Security = new SecurityMessageProperty() { ServiceSecurityContext = securityContext };
     }
   }

   class PrincipalAuthorizationPolicy : IAuthorizationPolicy
   {
     string id = Guid.NewGuid().ToString();
     IPrincipal user;

     public PrincipalAuthorizationPolicy(IPrincipal user)
     {
       this.user = user;
     }

     public ClaimSet Issuer
     {
       get { return ClaimSet.System; }
     }

     public string Id
     {
       get { return this.id; }
     }

     public bool Evaluate(EvaluationContext evaluationContext, ref object state)
     {
       evaluationContext.AddClaimSet(this, new DefaultClaimSet(System.IdentityModel.Claims.Claim.CreateNameClaim(user.Identity.Name)));
       evaluationContext.Properties["Identities"] = new List<IIdentity>(new IIdentity[] { user.Identity });
       evaluationContext.Properties["Principal"] = user;
       return true;
     }
   }

A WCF Data Service, as any other WCF Service, contains a service host where this interceptor can be injected. The following code illustrates how that can be done in the “svc” file.

<%@ ServiceHost Language="C#" Debug="true" Service="ContactsDataService"
                Factory="AppServiceHostFactory" %>

using System;
using System.ServiceModel;
using System.ServiceModel.Activation;
using Microsoft.ServiceModel.Web;

class AppServiceHostFactory : ServiceHostFactory
{
   protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
  {
    WebServiceHost2 result = new WebServiceHost2(serviceType, true, baseAddresses);

    result.Interceptors.Add(new SamlAuthenticationInterceptor());
           
    return result;
  }
}

WCF Data Services includes an specific WCF host of out the box (DataServiceHost). However, the service is not affected at all if you replace it with a custom one as I am doing in the code above (WebServiceHost2 is part of the REST Starter kit).

Finally, the client application needs to pass the SAML token somehow to the data service. In case you are using any Http client library for consuming the data service, that’s easy to do, you only need to include the SAML token as part of the “Authorization” header. If you are using the auto-generated data service proxy, a little piece of code is needed to inject a SAML token into the DataServiceContext instance. That class provides an event “SendingRequest” that any client application can leverage to include custom code that modified the Http request before it is sent to the service. So, you can easily create an extension method for the DataServiceContext that negotiates the SAML token with an existing STS, and adds that token as part of the “Authorization” header.

public static class DataServiceContextExtensions
{     

  public static void ConfigureFederatedCredentials(this DataServiceContext context, string baseStsAddress, string realm)
  {
    string address = string.Format(STSAddressFormat, baseStsAddress, realm);
            
    string token = NegotiateSecurityToken(address);
    context.SendingRequest += (source, args) =>
    {
      args.RequestHeaders.Add("Authorization", token);
    };
  }

private string NegotiateSecurityToken(string address)
{
}

}

I left the NegociateSecurityToken method empty for this extension as it depends pretty much on how you are negotiating tokens from an existing STS. In case you want to end-to-end REST solution that involves an Http endpoint for the STS, you should definitely take a look at the Thinktecture starter STS project in codeplex.

Posted by cibrax | 4 comment(s)

I discussed a time ago how WIF supported a new WS-Trust 1.4 element, “ActAs”, and how that element could be used for authentication delegation.  The thing is that there is another feature in WS-Trust 1.4 that also becomes handy for this kind of scenario, and I did not mention in that last post, “OnBehalfOf”.

Shiung Yong wrote an excellent summary about the difference of these two new features in this forum thread. He basically commented the following,

“An ActAs RST element indicates that the requestor wants a token that contains claims about two distinct entities: the requestor, and an external entity represented by the token in the ActAs element.

An OnBehalfOf RST element indicates that the requestor wants a token that contains claims only about one entity: the external entity represented by the token in the OnBehalfOf element.

In short, ActAs feature is typically used in scenarios that require composite delegation, where the final recipient of the issued token can inspect the entire delegation chain and see not just the client, but all intermediaries to perform access control, auditing and other related activities based on the whole identity delegation chain. The ActAs feature is commonly used in multi-tiered systems to authenticate and pass information about identities between the tiers without having to pass this information at the application/business logic layer.

OnBehalfOf feature is used in scenarios where only the identity of the original client is important and is effectively the same as identity impersonation feature available in the Windows OS today. When the OnBehalfOf is used the final recipient of the issued token can only see claims about the original client, and the information about intermediaries is not preserved. One common pattern where OnBehalfOf feature is used is the proxy pattern where the client cannot access the STS directly but is instead communicating through a proxy gateway. The proxy gateway authenticates the caller and puts information about him into the OnBehalfOf element of the RST message that it then sends to the real STS for processing. The resulting token is going to contain only claims related to the client of the proxy, making the proxy completely transparent and not visible to the receiver of the issued token.”

Going back to WIF, “ActAs” and “OnBehalfOf” are both supported as extensions methods in the WCF client channel.

public static class ChannelFactoryOperations
{
  public static T CreateChannelActingAs<T>(this ChannelFactory<T> factory,
    SecurityToken actAs);
 
  public static T CreateChannelOnBehalfOf<T>(this ChannelFactory<T> factory,
    SecurityToken onBehalfOf);
}

Both methods receive the security token with the identity of the original caller.

Posted by cibrax
Filed under: , ,
More Posts