Multi tenant federation with Geneva Framework and Microsoft .NET Services Access Control

A typical scenario for an ISV that wants to create the "next application in the cloud" will be how to support identity federation with their customers (tenants). A common requirement I've heard is:

"I want to enable single sign on and allow enterprises that have their own STS to integrate with us. For companies that don't have any identity infrastructure in place we want to allow them to login with an ubiquous credential like Windows LiveID. How do we do that without spending three months with a security guru?"

A possible answer is use Microsoft .NET Services Access Control. They enable that scenario in a very straightforward fashion. The following diagram shows a possible architecture that might fulfill the customer requirements. In this picture Southworks is an enterprise that has its own STS and Contoso doesn't, hence they use Windows LiveID for their users. The good thing about this is that in the middle we have ACS acting as the "normalizer". It will receive tokens from LiveID and Southworks IP STS and will transform them to something Fabrikam knows (Roles, Actions, etc.).

image

If you are like me, you might be wondering how this all works. Here are the gory details of all the HTTP interactions of a WS-Federation passive profile "dance":

  1. A user opening a browser
    1. If he is at Contoso he browses to www.fabrikam-cloudapp.com/contoso (or contoso.fabrikam-cloudapp.com)
    2. If he is at Southworks he browses to www.fabrikam-cloudapp.com/southworks (or southworks.fabrikam-cloudapp.com)
  2. At the web site there is an Http Module or ActionFilter in ASP.NET MVC that will read the tenant alias from the route.
  3. The module will construct the SignInRequestMessage federation message and will redirect to ACS (https://{solution-name}.accesscontrol.windows.net/passivests/{federation-endpoint}).
    1. If it's Southworks tenant we have to use the https://{solution-name}.accesscontrol.windows.net/passivests/Federation.aspx endpoint of ACS.
    2. If it's Contoso we have to use https://{solution-name}.accesscontrol.windows.net/passivests/LiveFederation.aspx in the endpoint of ACS.
      Note: these urls are not in any ACS documentation for now
    3. The homeRealm (whr) parameter will tell ACS which IP STS to use (Contoso = login.live.com, Southworks = login.southworks.net which is a url Southworks provided at provisioning probably).
    4. Finally, the realm parameter is fabrikam-cloudapp application. This will have to match with the scope you create on ACS (more on that below).
      Note: Look at the code at the end of the post to see how homeRealm and realm is used.
  4. ACS will redirect the user to either LiveID or the company IP STS depending on the whr parameter
  5. The user will login with LiveID cred or with other mechanisms if it's a Geneva Server (user/pwd, kerberos, certs, cardspace, etc.)
  6. A token will be issued and will be POSTed to ACS federation endpoint
    1. [Contoso] In the case of LiveID you don't have to do anything because ACS setup all the federation for us. LiveID will issue a token with one claim: the WLID email.
    2. [Southworks] In the case of the company IP STS you will have to configure ACS as a relying party and supply its certificate to encrypt the token in your IP STS. In ACS you will have to create an Issuer and upload the company IP STS certificate. The company STS will issue the name claim and maybe groups or other claims.
  7. ACS will read the token and apply the claim transformation (more on that below)
  8. ACS will POST the token to www.fabrikam-cloudapp.com
  9. Geneva Fx on the website will read the token, generate the ClaimsPrincipal and store it in a cookie for later usage.
  10. Fabrikam can now authorize access to certain pages by reading the principal! either with IsInRole or for granular checks you could use Actions or a ClaimsAuthorizationManager

This is how ACS is configured for :

ACS scope configuration

A better way to look at the table above is the following diagram:

Claim mappings diagram

Here we are using the Geneva Fx manually inside an ASP.NET MVC action filter. We use it manually because the multi tenancy nature that we want to implement does not allow to use the fixed values from the wsFederation configuration section.

   1: namespace FabrikamCloudApp.Web.Identity
   2: {
   3:     using System;
   4:     using System.Globalization;
   5:     ...
   6:  
   7:     [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
   8:     public sealed class WSFederationAuthenticationFilterAttribute : ActionFilterAttribute, IExceptionFilter
   9:     {
  10:         public override void OnActionExecuting(ActionExecutingContext filterContext)
  11:         {            
  12:             var principal = Thread.CurrentPrincipal;
  13:  
  14:             if (!principal.Identity.IsAuthenticated)
  15:             {
  16:                 string realm;
  17:                 string homeRealm;
  18:                 string acsEndpoint;
  19:                 SignInRequestMessage message = null;
  20:                 
  21:         // retrieve tenant from url (http://.../{tenant})
  22:                 var tenant = (string)filterContext.RouteData.Values["tenant"];
  23:         
  24:         // lookup in some repository if the tenant setup a federation with its own sts
  25:                 if (IsLiveIDFederation(tenant)) 
  26:                 {
  27:                     acsEndpoint = "LiveFederation.aspx";
  28:                     homeRealm = "http://login.live.com";
  29:                 }
  30:                 else
  31:                 {
  32:                     acsEndpoint = "Federation.aspx";
  33:                     homeRealm = GetIdentityProviderUrl(tenant);
  34:                 }
  35:  
  36:                 realm = "http://www.fabrikam-cloudapp.com/";
  37:                 string issuer = "https://{solution-name}.accesscontrol.windows.net/passivests/{federation-endpoint}"
  38:                                 .Replace("{solution-name}", Configuration.FabrikamSolutionName)
  39:                                 .Replace("{federation-endpoint}", endpoint);
  40:  
  41:                 message = new SignInRequestMessage(new Uri(issuer), realm);
  42:                 message.Parameters.Add("whr", homeRealm);
  43:  
  44:         // redirect to ACS
  45:                 filterContext.Result = new RedirectResult(message.WriteQueryString());
  46:             }
  47:             else
  48:             {
  49:                 // we are back on our site, and we've got our token transformed to ClaimsPrincipal
  50:         var claimsIdentity = principal.Identity as IClaimsIdentity;
  51:  
  52:                 var tenantClaim = claimsIdentity.Claims.FirstOrDefault(c => c.ClaimType == Configuration.TenantClaimType);
  53:                 if (tenantClaim == null)
  54:                 {
  55:                     throw new HttpException(401, "Access is denied");
  56:                 }
  57:                 
  58:                 var fam = FederatedAuthentication.WSFederationAuthenticationModule;                                
  59:                 if (fam.CanReadSignInResponse(HttpContext.Current.Request, true))
  60:                 {
  61:                     SecurityToken token = fam.GetSecurityToken(HttpContext.Current.Request);
  62:                     SessionSecurityToken sessionToken = new SessionSecurityToken(principal as IClaimsPrincipal, token);
  63:                     fam.SetPrincipalAndWriteSessionToken(sessionToken, true);
  64:                     
  65:                     filterContext.Result = new RedirectToRouteResult(
  66:                                                         "DefaultRoute",
  67:                                                         new RouteValueDictionary(new { tenant = tenantClaim.Value }));
  68:                 }                
  69:             }
  70:         }
  71:         
  72:     }
  73: }

And just put this attribute on your entry point Controller:

   1: public class HomeController 
   2: {
   3:     [WsFederationAuthenticationFilter]
   4:     public void Index()
   5:     {
   6:     }
   7: }

I can tell my customer "sure, we can implement this. It's only 73 lines of code and some configuration here and there" :)

Happy Federation!

blog comments powered by Disqus