Sitecore 7.0 with Windows Identity Foundation 4.5 security

Recently I found myself on a project with the task to implement signin for a new intranet platform based on Sitecore 7.0, using MVC and .NET 4.5 running in the Windows Azure cloud platform. Per requirements the end customer didn’t want to maintain user information within Sitecore but use multiple ADFS 2.0- and other domains for authentication. The Azure Access Control Services (ACS) would be the central gateway for user authentication.

The requirements called for a federated security model combined with Sitecore virtual users. Since the platform is all .NET 4.5 the logical choice for implementing the federated security was the Windows Identity Foundation (WIF). For using WIF within a .NET application Microsoft already provides a lot of examples, many of them not even requiring code but using configuration only. However almost all of these examples apply to a more or less standard .NET application and won’t work within a Sitecore environment. Main reason for this is differences between the .NET 4.5 claim-based security implementation and the Sitecore security model. Also with .NET 4.5 WIF has been fully integrated into the .NET framework core and therefore has some differences with earlier versions.

WIF, ACS and Sitecore

In this post I’ll explain a way on how to implement WIF security in a Sitecore 7.0/ MVC environment. For this article I assume the reader is familiair with terms and abbreviations used in federated security and WIF. If not there’s plenty of information to be found on the net. I also won’t go into details of setting up ACS itself or (the trusts with) ADFS 2.0 domains. A detailed explanation of how this works can be found at http://azure.microsoft.com/en-us/documentation/articles/active-directory-dotnet-how-to-use-access-control/ and related articles.

For the rest of this article you should have created and configured an ACS namespace for you or your organization with at least one identity provider. For legal reasons any code, configuration and references here are examples and not from the actual project. In production environments more exception- and security handling is required.

Steps involved

In our implementation, when a user that’s not yet authenticated goes to the site, the following main steps take place:

  1. Our system makes a request to the ACS to retrieve the list with information for the configured identity providers. The user is redirected to our “login” page, which is similar to normal forms authentication except the user is presented with the list of identity providers instead of a username/ password page, and has to pick the provider of his or her choice;
  2. After picking a provider, the system calls the login url that came with the information from the ACS. If the user is already authenticated with this provider, it immediately returns a security token with a claimset. If not, the user is presented with a login page or box by this provider and has to log in;
  3. The token and claimset is returned through the ACS which may or may not transform or add any information, depending on how it is configured. The ACS returns a security token and the claimset to our system;
  4. WIF intercepts the returned information and performs the necessary checks and steps. See the MSDN pages on “WSFederationAuthenticationModule” for more information;
  5. Our system retrieves the information (claims) through the WIF modules.
  6. With this information we create a Sitecore virtual user, add the necessary roles and attributes to it and log in.

Step 1: Get a list of registered (trusted) IP’s from ACS

First step is to get the configured providers information from our ACS. This can be done by a call to a Javascript endpoint that exists on the ACS. This call can have a bunch of query parameters, of which three are required:

  • protocol, in this case wsfederation
  • version, in this case 1.0
  • realm, the url of your (future) web application that has been configured in your ACS portal as a RP (Relying Party) application.

Let’s say we have configured http://localhost/ on our ACS as relying party, it will look like the following:


https://namespace.accesscontrol.windows.net/v2/metadata/IdentityProviders.js?protocol=wsfederation&version=1.0&realm=http://localhost

where namespace is the namespace that you registered with ACS for you or your organization. This call will return a JSON structure containing an array of objects (one for each configured identity provider) that translates to the following C# class:

[Serializable]
public class IdentityProvider
{
  public List<string> EmailAddressSuffixes { get; set; }
  public string ImageUrl { get; set; }
  public string LoginUrl { get; set; }
  public string LogoutUrl { get; set; }
  public string Name { get; set; }
}

Note that the returned JSON structure uses Microsoft C# naming convention and not the common Javascript convention. When using JSON.NET to deserialize the response the code for sending the request and getting the result will look like the following:

 List<IdentityProvider> Providers;
 using (System.Net.WebClient webClient = new System.Net.WebClient())
 {
   webClient.Encoding = System.Text.Encoding.UTF8;
   string jsonResponse = webClient.DownloadString(requestString);
   Providers = JsonConvert.DeserializeObject<List<IdentityProvider>>(jsonResponse);
 }

Where RequestString must contain the request as shown before. We used a simple form with a submit button and a dropdown box. The dropdown listed the Name property of each provider, and used the serialized provider object for value so we didn’t have to store anything in session variables or hidden fields, keeping our application stateless as recommended for MVC applications.

Step 2: Request authentication from chosen IP

Once the user selected an identity provider (IP), we deserialize the value back to our Provider object and use the value of the LoginUrl property to request authentication from that IP. In an MVC environment we can do this very easily by returning a Redirect action result to that URL. The LoginUrl property should contain the full URL (including going through the ACS) with all information required. Let’s say the user selected an IP and the submit action calls this Controller method (The SelectedIdentityProvider parameter should contain the value property of the chosen provider from the dropdown):

[HttpPost]
[System.Web.Http.AllowAnonymous]
public ActionResult ProviderSelected(string SelectedIdentityProvider)
{
IdentityProvider provider;
…
//Code to retrieve the SelectedIdentityProvider object and assign it to provider
…
return Redirect(provider.LoginUrl);
}

If not yet authenticated the user should be presented with a login box or screen by that IP. Once authenticated, the IP returns the issued security token to our ACS namespace which was set as the wreply parameter in the Login URL.

Step 3: Returning the security token and claims

On the ACS portal we should have configured our Sitecore application as relying party and set the “Return URL” field to the URL of a controller method that handles further login. Optionally we can set a “Error URL” and implement an error handling controller method in case something went wrong on the IP side.

The ACS calls back to our application on the return URL. This return call should be intercepted and processed by the WIF modules (see next step) and then WIF actually calls the return URL on our application. This processing involves validating the returned token and then creating a ClaimsPrincipal, using this to create a session security token. Because the WIF modules reside in the ASP.NET pipeline the security can be implemented in a standard .NET application using configuration only. However this ClaimsPrincipal is an IPrincipal implementation and this is where the problem arises within a Sitecore 7.0 environment, since Sitecore security and users do not derive (yet?) from this claims model.

Step 4: Setting up WIF to process the returned security token and claims

The core modules here are the WSFederationAuthenticationModule and the SessionAutentication modules, which both exist as properties on the FederationAutentication static class. In .NET 4.5 the classes reside in the System.IdentityModel.Services Namespace. You need to add references to System.IdentityModel and System.identityModel.Services in your project. Note that the second reference may (accidentally?) contain a lowercase “i”, violating the usual Microsoft naming convention.

We derived our own ScFederationAuthenticationModule and ScSessionAuthentication from these classes since in both(!) classes we need to override the InitializeModule, the InitializePropertiesFromConfiguration and the OnAuthenticateRequest methods. We define two boolean properties moduleInitialized and propertiesInitialized on each of our our derived classes. See also http://msdn.microsoft.com/en-us/library/system.identitymodel.services.httpmodulebase.init(v=vs.110).aspx for this.

protected override void InitializeModule(System.Web.HttpApplication context)
{
  if (this.moduleInitialized) return;
  this.moduleInitialized = true;
  base.InitializeModule(context);
}

protected override void InitializePropertiesFromConfiguration()
{
  if (this.propertiesInitialized) return;
  this.propertiesInitialized = true;
  base.InitializePropertiesFromConfiguration();
}

protected override void OnAuthenticateRequest(object sender, EventArgs args)
{
  // Skip event if Sitecore user already authenticated.
  if (Sitecore.Context.User != null && Sitecore.Context.User.IsAuthenticated)
  {
    return;
  }
  base.OnAuthenticateRequest(sender, args);
}

The overrides are necessary to prevent WIF from interfering after we have created and signed in our (virtual) user in Sitecore.

The WIF modules need to be in the ASP.NET pipeline so they need to be added to web.config. Under the <modules> node in <system.webServer> add the following 2 entries:

<add name="WSFederationAuthenticationModule" type="SitecoreFedSecurity.ScFederationAuthenticationModule, SitecoreFedSecurity" />
<add name="SessionAuthenticationModule" type="SitecoreFedSecurity.ScSessionAuthenticationModule, SitecoreFedSecurity" />

with SitecoreFedSecurity being the namespace and assembly name for our derived classes. These entries need to be right after the Sitecore.Nexus.Web.HttpModule entry.

We then need to define 2 configuration sections for these modules:

<section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />

And the definitions of these sections:

<system.identityModel>
  <identityConfiguration>
    <audienceUris>
      <add value="http://localhost/" />
    </audienceUris>
    <securityTokenHandlers>
      <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
      <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </securityTokenHandlers>
    <certificateValidation certificateValidationMode="None" />
    <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
    <trustedIssuers>
      <add thumbprint="[certificate thumbprint]" name="https://sionict.accesscontrol.windows.net/" />
    </trustedIssuers>
  </issuerNameRegistry> 
</identityConfiguration>
</system.identityModel>
<system.identityModel.services>
  <federationConfiguration>
    <cookieHandler requireSsl="false" />
    <wsFederation passiveRedirectEnabled="false" issuer="https://[namespace].accesscontrol.windows.net/v2/wsfederation" realm="http://localhost/" requireHttps="false" persistentCookiesOnPassiveRedirects="false" />
  </federationConfiguration>
</system.identityModel.services>

This configuration is explained in various articles about WIF so I won’t go into detail here. A note though about the issuerNameRegistry node: there seems to be two variants, of which this is the one that currently seems to work. Previously you needed to add System.IdentityModel.Tokens.ValidatingIsserNameRegistry from NuGet (which has a different setup) to your project and which can be found in various examples on the net, but that doesn’t seem to work anymore. See “http://stackoverflow.com/questions/23692326/adfs-2-0-microsoft-identityserver-web-invalidrequestexception-msis7042.

Replace the references to localhost with your own application’s URL if it is different. There’s two other things here specific for your situation: [namespace] should be replaced with your specific ACS namespace, and [certificate thumbprint] is the thumbprint of the X.509 certificate for your ACS namespace. It can be found in the ACS portal under “Certificates and Keys”.

Aside from the above configuration your application should expose a FederationMetadata.xml which simplifies maintenance on the ACS. See the Microsoft documentation on this. The location of this file and a few other paths need to be accessible for any (anonymous) user. See the Notes at the end of this post.

Step 5: Retrieving claims information

Now that we have WIF set up in our system, we can implement the Controller method we have set as return URL on the ACS to retrieve the information from the claimset. Since we need to create and authenticate a Sitecore user, we need to retrieve the necessary information from WIF and perform a few checks. In our case we named the controller method for the return URL SignIn:

public string SignIn()
{
  bool result = false;
  System.IdentityModel.Tokens.SessionSecurityToken sessionToken = null;
  System.Security.Claims.ClaimsPrincipal claimsPrincipal = null;
  try
  {
    result = ((SCSessionAuthenticationModule)FederatedAuthentication.SessionAuthenticationModule).TryReadSessionTokenFromCookie(out sessionToken);
    if (result) claimsPrincipal = sessionToken.ClaimsPrincipal;
  }
  catch (System.Exception ex)
  {
    return string.Format("Could not retrieve session security token cookie. Could not create user. Exception: {0}", ex.Message);
  }
  //Check status
  if ((claimsPrincipal == null) || (claimsPrincipal.Identity == null))
  {
    return "No claimsPrincipal is set. Could not create user";
  }
  if (!claimsPrincipal.Identity.IsAuthenticated)
  {
    return string.Format("Chosen identity provider did not authenticate identity {0}", claimsPrincipal.Identity.Name);
  }
  //TODO: Create a virtual user based on the principal
}

We access the securitytoken cookie set by WIF through the TryReadSessionTokenFromCookie method of the SessionAuthenticationModule. Despite the “Try..” naming of the method it still throws an exception if the cookie could not be read, so you need to add exception handling here. After getting the token you need to verify the principal is present, and the user is actually authenticated by the IP.

Step 6: Creating the (virtual) Sitecore user and log in

Now that we have the information from the identity provider we can create and log in our Sitecore virtual user. Replace the “TODO” comment in the above code with the following:

string identifier = (string.IsNullOrWhiteSpace(claimsPrincipal.Identity.Name)) ?
  		claimsPrincipal.Claims.FirstOrDefault().Value :
		  claimsPrincipal.Identity.Name;
Sitecore.Security.Accounts.User user = Sitecore.Security.Authentication.AuthenticationManager.BuildVirtualUser("extranet\\" + identifier, true);
//Add any roles or attributes for the user here, before login
Sitecore.Security.Authentication.AuthenticationManager.LoginVirtualUser(user);
return string.Empty;

The Name property should be set by WIF from the corresponding claim, but not all identity providers include a name in the claimset so it can be null. Windows Live for example only returns an unique ID. In our example here we pick the first claim from the set but which claim you need depends on your situation. We also haven’t set any roles or additional properties here but that should be pretty straightforward using the Sitecore API.

Signing out

Besides a login URL, the identity provider information also contains a signout URL we can use to sign out the user with the chosen IP. Completely signing out can involve 4 steps:

  • Sign out of Sitecore with AuthenticationManager.Logout();
  • Sing out WIF with the SignOut() method on the WSFederationAuthenticationModule (or rather our derived class);
  • Sign out with the identity provider with WSFederationAuthenticationModule.FederatedSignOut(..), using the signout URL;
  • Sign out with ACS using WSFederationAuthenticationModule.GetFederationPassiveSignOutUrl(..)

Federated security and signing out can be problematic. It is up to the identity provider if and how to process a signout request, and it may not be possible to sign out because it completely ignores these requests. Other providers abort the above sequence because they do not return after the signout request but display a message page instead. There has been quite some criticism towards Microsoft also for providing plenty of examples for federated security signin but little examples about signing out. Be aware that especially in public environments, even after the above steps (and even after closing the browser as some providers instruct you to do!), the user may not be signed out by the IP, causing an automatic authentication without having to login on a subsequent session.

Notes

A few things must be kept in mind when implementing this security model:

  • The Sitecore CMS still needs the built-in users to be able to access through the CMS login page. Also the FederationMetaData should be accessible, and possibly some other paths containing styles, images or scripts. We use the <location> configuration setting to give access to all users on these folders:
    <location path="FederationMetadata">
      <system.web>
        <authorization>
          <allow users="*" />
        </authorization>
      </system.web>
    </location>
    

    Unfortunately the <location> setting can take only one path so you need to create an entry like this for every path.

  • Make sure you have set up MVC routing properly for your Sitecore environment for the callbacks from the ACS to work.
  • Both WIF modules contain an OnAuthenticateRequest. As it turns out this name is somewhat confusing as it is called on every request, and the actual check whether or not it is a request for authentication is done within the (base) implementation of this method.
  • When hooking up WIF events, be aware that the WIF modules are alive al long as the session is active because they are set as properties on the (static) FederationAutentication class, but MVC objects like controllers are disposed between calls. So when WIF is processing a request and firing the various events before calling the return URL, there is no MVC controller alive.
  • Since this all involves security and users, I got remarks that there should be an ASP.NET MembershipProvider somewhere. It is possible to implement certain parts in methods of a custom MembershipProvider if it needs to be enriched with information from Sitecore or another local storage. Do realize membershiproviders are nothing more than an abstraction between user information storage and applications, and within a federated security model this is is all delegated to the identity provider.

5 thoughts on “Sitecore 7.0 with Windows Identity Foundation 4.5 security

    1. Marcel C.R. van Driel Post author

      The code shown in the boxes is all the code you need to get it running. The sample project I created for this article is tied to my own Sitecore installation (references and such).
      You can copy/ paste the code from here in the appropriate places in your own (Visual Studio/ Sitecore) project. All you have to add yourself is a form where your user can make the provider selection (instead of showing a username/ password form) and fill in the code in ProviderSelected in step 2 to retrieve the selected provider, as described.

      Like

      Reply
  1. Ramesh

    Hi Marcel,

    Thanks for your post, it’s very helpful to me. I’m implementing Authentication mechanism using ACS & ADFS, and able to successfully implement the logic, get the logged in user claims.

    But post that I’m trying to create virtual user based on the logged in claims (I am using sitecore 7.5, visual studio 2012/13) getting the below error message:

    “An exception of type ‘System.Exception’ occurred in Sitecore.Kernel.dll but was not handled in user code
    Additional information: Cannot load provider. Base type is ‘Sitecore.Security.Authentication.AuthenticationProvider”

    Can you please help me on this?
    If any possibility please try to share the code.

    Thanks,
    Ramesh

    Like

    Reply
    1. Marcel C.R. van Driel Post author

      Sorry for the very late reply but somehow I never got a notification of your post.

      Hard to tell from here, the exception gives little information. Usually this happens with a mismatch in library versions or it is a problem with not having enough permissions to execute the code.

      Like

      Reply

Leave a comment