Sitecore with ClaimsIdentity

This post is an adventures experience to explore if Sitecore can work with ClaimsIdentity and make my extranet authentication Claims aware, with little experiment I could able to get both application (extranet) and Sitecore working but it requires more testing before using this in a live application.

All below sections in this article talk around extranet user authentication aspect though Sitecore CMS authentication will just work fine with by using existing Forms Authentication.

Overview

This article is result of a problem which I had in one of my Sitecore implementation where I was working with HttpModule based security implementation similar to Windows Identity Foundation but slight different implementation tied to a custom DB based user store, HttpModule looks after security aspect of Application without any custom implementation within application, which is good as security should not be of application concern. I ran a custom dummy implementation of Membership Provider which will read the ClaimsIdentity returned by module and make Sitecore aware that current user is authenticated. In Application, Sitecore’s Identity/Principal will take over the HttpContext’s User object which I was overwriting through MVC’s ActionFilter applied on my controller/action but it resulted into a race between Sitecore Security and HttpModule to take over the User object based on context.

How Sitecore Security works

Sitecore’s security model is based on the ASP.NET Membership Provider model and it has three main providers, Membership Provider, Role Provider and Profile Provider, Sitecore’s Security Reference document talks about in details how all three works. From implementation point of view Sitecore implements IIdentity interface as SitecoreIdentity and IPrincipal as User, under the hood it is supported by Authentication Provider and Authentication Helper implementation for different purpose.

Also, Sitecore support different authentication options outlined by John West here. Though all of them implement Membership Provider internally to connect with respective authentication source.

Problem

With changes introduced in .NET 4.5 now all Identity and Principal implementation derives from ClaimsIdentity and ClaimsPrincipal which implements IIdentity and IPrincipal. As Sitecore directly implements these interfaces, it is not possible to utilize the Claims with Sitecore Identity and User (Principal).

Also, with OpenId Connect and OAuth2 being the future of authentication and authorization, it is not possible to scale up with Membership Model. We can always implement a custom Provider to call these services but it will not be able to support Claims.

Back to Sitecore Security

When application calls the Sitecore’s AuthenticationManager to Login a user, it does following in the background.

  1. Application calls – AuthenticationManager.Login method with User Name and Password or just User Name
  2. AuthenticationManager will call the registered AuthenticationProvider’s Login method, by default it will be FormsAuthenticationProvider which inherits bunch of functionality from MembershipAuthenticationProvider
  3. AuthenticationProvider will intern calls the respective AuthenticationHelper to validate the user credential, which relies on MembershipProvider to validate it
  4. Once validation is successful FormsAuthenticationProvider will drop the FormAuth cookie for requested user
  5. On subsequent access, AuthenticationProvider will call GetActiveUser which intern calls AuthenticationHelper’s GetCurrentUser method to identify/retrieve the current user from Current Thread or Current Context User or finally from FormAuth cookie

Make Sitecore aware about ClaimsIdentity

By looking at the above steps of Sitecore execution, it gives enough indication that I need following components to make Sitecore ClaimsIdentity aware.

  1. A switching provider to switch between AuthenticationProvider for Sitecore and extranet logins
  2. A mapping of different Sitecore domains with corresponding AuthenticationProviders
  3. A custom AuthenticationProvider to Validate and drop the security token via cookie using WIF’s SessionAuthenticationModule – I prefer user credential validation via my action method and service call to user store
  4. A corresponding AuthenticationHelper to read session cookie and identify logged-in user

along with above, I need WIF specific implementation to extend Claims collection for the incoming principal.

  1. A ClaimsAuthenticationManager implementation to validate/filter/extend Claims
  2. A HttpModule to run ClaimsAuthenticationManager on PostAuthenticateRequest event

The most liked feature of Sitecore by developer community is it’s extensibility and Sitecore allows to customize most of the behavior within. In this case, I am using a Sitecore hidden gem ‘SwitchingAuthenticationProvider’. Switcher is common with Membership provider where you can rely on two different Membership Providers for login in different context, e.g. Sitecore’s core DB for Sitecore CMS login but Active Directory for Application login. SwitchingAuthenticationProvider allows to plug-in different authentication provider for different context e.g. I can use FormsAuthenticationProvider to Sitecore’s authentication and my custom implementation for extranet authentication. Sitecore’s default SwitchingAuthenticationProvider didn’t worked as expected and I could not able to load the domain, provider mappings, so I extended the SwitchingAuthenticationProvider with my custom implementation.

 public class SwitchingAuthenticationProviderExtension : SwitchingAuthenticationProvider
    {
        protected AuthenticationProvider CurrentProvider
        {
            get
            {
                var baseType = typeof(SwitchingAuthenticationProviderExtension).BaseType;
                if (baseType != null)
                {
                    var provider = baseType.GetProperty("CurrentProvider", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly);
                    return provider.GetValue(this) as AuthenticationProvider;
                }

                return null;
            }
        }
        
        public override User GetActiveUser()
        {
            return CurrentProvider == null ? null : CurrentProvider.GetActiveUser();
        }
    }

I have my Authentication Switcher and need a new Authentication Provider i.e. ClaimsAuthenticationProvider for extranet authentication and native Sitecore’s FormsAuthenticationProvider for Sitecore CMS authentication.

The current implementation derives from MembershipAuthenticationProvider just to avoid various abstract method implementation but best would be to directly inherit from abstract AuthenticaitonProvider class. There are two method which you might be interested in

  1. Login – This method is responsible to create ClaimsIdentity and ClaimsPrincipal for requested user and drop the authentication cookie via SessionAuthenticationModule.
  2. Logout – This method will call the Signout method from SessionAuthenticationModule.
public class ClaimsAuthenticationProvider : MembershipAuthenticationProvider
    {
        #region Fields

        private ClaimsAuthenticationHelper _helper;

        #endregion

        #region Properties

        protected override AuthenticationHelper Helper
        {
            get
            {
                var authenticationHelper = _helper;
                Assert.IsNotNull(authenticationHelper, "AuthenticationHelper has not been set. It must be set in Initialize.");
                return authenticationHelper;
            }
        }

        #endregion

        #region MembershipAuthenticationProvider Overrides

        public override void Initialize(string name, NameValueCollection config)
        {
            Assert.ArgumentNotNullOrEmpty(name, "name");
            Assert.ArgumentNotNull(config, "config");

            base.Initialize(name, config);
            _helper = new ClaimsAuthenticationHelper(this);
        }

        public override User GetActiveUser()
        {
            var activeUser = Helper.GetActiveUser();
            Assert.IsNotNull(activeUser, "Active user cannot be empty.");
            return activeUser;
        }

        public override bool Login(string userName, bool persistent)
        {
            Assert.ArgumentNotNullOrEmpty(userName, "userName");

            SessionSecurityToken sessionToken;
            if (!FederatedAuthentication.SessionAuthenticationModule.TryReadSessionTokenFromCookie(out sessionToken))
            {
                var claims = new[] { new Claim(ClaimTypes.Name, Globalize(Context.Domain.Name, userName))};
                var id = new ClaimsIdentity(claims, "Forms");
                var cp = new ClaimsPrincipal(id);

                var token = new SessionSecurityToken(cp);
                var sam = FederatedAuthentication.SessionAuthenticationModule;
                sam.WriteSessionTokenToCookie(token);
            }

            return true;
        }

        public override bool Login(User user)
        {
            Assert.ArgumentNotNull(user, "user");

            return Login(user.Name, false);
        }

        public override void Logout()
        {
            SessionSecurityToken sessionToken;
            if (!FederatedAuthentication.SessionAuthenticationModule.TryReadSessionTokenFromCookie(out sessionToken))
            {
                // Clean up
            }
            base.Logout();
            FederatedAuthentication.SessionAuthenticationModule.SignOut();
        }

        public override void SetActiveUser(User user)
        {
            Helper.SetActiveUser(user);
        }

        public override void SetActiveUser(string userName)
        {
            Assert.ArgumentNotNullOrEmpty(userName, "userName");
            Helper.SetActiveUser(userName);
        }

        #endregion

        #region Methods

        private static string Globalize(string domainName, string userName)
        {
            var str = userName;
            if (!userName.StartsWith(domainName + "\\"))
                str = domainName + "\\" + userName;
            return str;
        }

        #endregion
    }

In order to support the Provider, I need AuthenticationHelper implementation, here ClaimsAuthenticationHelper relies on 3 options to retrieve the logged-in user, first it will look for executing ThreadPrincipal to determine the user, second it will look for HttpContext.User to find out the logged-in user else it will fallback to read the authentication cookie.

public class ClaimsAuthenticationHelper : AuthenticationHelper
    {
        #region Constructor
       
        public ClaimsAuthenticationHelper(AuthenticationProvider provider)
            : base(provider)
        {
        }

        #endregion

        #region AuthenticationHelper Overrides

        public override void SetActiveUser(User user)
        {
            Assert.ArgumentNotNull(user, "user");

            var name = user.Name;
            if (!name.Contains("\\"))
                Globalize(Context.Domain.Name, name);
            base.SetActiveUser(user);
        }

        public override void SetActiveUser(string userName)
        {
            Assert.ArgumentNotNull(userName, "userName");
            var userName1 = userName;
            if (!userName1.Contains("\\"))
                Globalize(Context.Domain.Name, userName1);
            base.SetActiveUser(userName);
        }

        #endregion

        #region Methods

        protected virtual bool IsDisabled(User user)
        {
            Assert.ArgumentNotNull(user, "user");

            return !user.Profile.IsAnonymous && user.Profile.State.Contains("Disabled");
        }

        protected override User GetCurrentUser()
        {
            HttpContext current = HttpContext.Current;
            if (current == null)
            {
                if (Thread.CurrentPrincipal != null)
                {
                    if (Thread.CurrentPrincipal is User)
                    {
                        return Thread.CurrentPrincipal as User;
                    }
                    if (!string.IsNullOrEmpty(Thread.CurrentPrincipal.Identity.Name))
                    {
                        //return base.GetUser(Thread.CurrentPrincipal.Identity.Name, Thread.CurrentPrincipal.Identity.IsAuthenticated);
                        return GetUser(Thread.CurrentPrincipal.Identity);
                    }
                }

                return null;
            }
            IPrincipal user = HttpContext.Current.User;
            if (user != null)
            {
                if (user is User)
                {
                    return user as User;
                }
                IIdentity identity = user.Identity;
                if (string.IsNullOrEmpty(identity.Name))
                {
                    return null;
                }
                //return base.GetUser(identity.Name, identity.IsAuthenticated);
                return GetUser(identity);
            }

            SessionSecurityToken sessionToken;
            FederatedAuthentication.SessionAuthenticationModule.TryReadSessionTokenFromCookie(out sessionToken);
            if (sessionToken != null && sessionToken.ClaimsPrincipal != null)
            {
                var identity = sessionToken.ClaimsPrincipal.Identity;
                if (!string.IsNullOrEmpty(identity.Name)) //&& User.Exists(Globalize(Context.Domain.Name, identity.Name)))
                    //return AuthenticationHelper.GetUser(Globalize(Context.Domain.Name, identity.Name), true);
                    return GetUser(sessionToken.ClaimsPrincipal);
            }

            return base.GetCurrentUser();
        }

        private static User GetUser(IPrincipal principal)
        {
            Assert.ArgumentNotNull(principal, "principal");

            return User.FromPrincipal(principal);
        }

        private static User GetUser(IIdentity identity)
        {
            Assert.ArgumentNotNull(identity, "identity");

            return User.FromPrincipal(new System.Security.Claims.ClaimsPrincipal(identity));
        }

        private new static User GetUser(string userName, bool isAuthenticated)
        {
            Assert.ArgumentNotNull(userName, "userName");

            return User.FromName(userName, isAuthenticated);
        }

        private static string Globalize(string domainName, string userName)
        {
            var str = userName;
            if (!userName.StartsWith(domainName + "\\"))
                str = domainName + "\\" + userName;

            return str;
        }

        #endregion
    }

Now, I have my authentication switcher, authentication provider and authentication helper ready, let’s map them up in configuration.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <authentication>
      <patch:attribute name="defaultProvider">switcher</patch:attribute>
      <providers>
        <add name="switcher" type="Framework.Sc.Extensions.Security.SwitchingAuthenticationProviderExtension, Framework.Sc.Extensions" patch:after="processor[@type='Sitecore.Security.Authentication.FormsAuthenticationProvider, Sitecore.Kernel']" domainMap="switchingProviders/authentication"/>
        <add name="claims" type="Framework.Sc.Extensions.Security.ClaimsAuthenticationProvider, Framework.Sc.Extensions" patch:after="processor[@type='Sitecore.Security.Authentication.FormsAuthenticationProvider, Sitecore.Kernel']" />
      </providers>
    </authentication>
    <switchingProviders>
      <authentication>
        <map provider="claims" storeFullNames="true" wildcard="%" domain="extranet"/>
        <map provider="forms" storeFullNames="true" wildcard="%" domain="sitecore"/>
        <!-- Above two configuration is sufficient for Sitecore 7.2 but doesn't work in Sitecore 8 and need below configuration for default domain -->
        <map provider="forms" storeFullNames="true" wildcard="%" domain="default"/>
      </authentication>
    </switchingProviders>
  </sitecore>
</configuration>

ClaimsAuthenticationManager

ClaimsAuthenticationManager is a place in pipeline to add/remove Claims before Application logic starts the processing. This pipeline will be called for every resource and I am using it to add more claims to requesting principal and save it as part of security token and intern as cookie so that it will be available in subsequent call.

public class ClaimsTransformer : ClaimsAuthenticationManager
    {
        public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
        {
            if (!incomingPrincipal.Identity.IsAuthenticated)
                return incomingPrincipal;

            var newPrincipal = Transform(incomingPrincipal);
            EstablishSession(newPrincipal);
            return newPrincipal;
        }

        protected virtual ClaimsPrincipal Transform(ClaimsPrincipal incomingPrincipal)
        {
            var identity = incomingPrincipal.Identity as ClaimsIdentity;
            if (identity != null && !incomingPrincipal.HasClaim(ClaimTypes.MobilePhone, "[some number]"))
            {
                //some values to see if Claims are available in next request correctly.
                identity.AddClaim(new Claim(ClaimTypes.MobilePhone, "[some number]"));
                identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
            }

            return incomingPrincipal;
        }

        private void EstablishSession(ClaimsPrincipal principal)
        {
            if (HttpContext.Current != null)
            {
                var sessionToken = new SessionSecurityToken(principal);
                FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionToken);
            }
        }
    }

A HttpModule to run the ClaimsAuthenticationManager, I don’t this is necessary as WIF’s Identity Configuation should call it by default without any extra code.

 public class ClaimsTransformationHttpModule : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            context.PostAuthenticateRequest += context_PostAuthenticateRequest;
        }

        void context_PostAuthenticateRequest(object sender, EventArgs e)
        {
            var context = ((HttpApplication)sender).Context;
            
            if (context == null)
                return;

            if (FederatedAuthentication.SessionAuthenticationModule == null)
                return;

            if (!FederatedAuthentication.SessionAuthenticationModule.ContainsSessionTokenCookie(context.Request.Cookies))
                return;

            var transformer = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager;

            if (transformer != null)
            {
                var identity = context.User.Identity as ClaimsIdentity;
                if (identity == null)
                    return;

                var transformedPrincipal = transformer.Authenticate(context.Request.RawUrl, new ClaimsPrincipal(identity));

                //context.User = transformedPrincipal;
                //Thread.CurrentPrincipal = transformedPrincipal;
            }
        }

        public void Dispose() { }
    }

Now, everything is setup except the WIF related configuration in Web.Config file and HttpModules configuration.

<system.identityModel.services>
    <federationConfiguration>
      <cookieHandler mode="Default" requireSsl="false" />
    </federationConfiguration>
  </system.identityModel.services>
  <system.identityModel>
    <identityConfiguration>
      <claimsAuthenticationManager type="Framework.Sc.Extensions.Security.ClaimsTransformer, Framework.Sc.Extensions" />
    </identityConfiguration>
  </system.identityModel>
 <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <remove name="WebDAVModule"/>
        <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
        <!-- All other HttpModules -->

Sample

        [HttpPost]
        public  ActionResult Login(LoginModel login)
        {
            if (ModelState.IsValid)
            {
                    bool success = new SimpleAuthenticationService().AuthenticateUser(login.UserName, login.Password); 
                    if(success)
                    {   
                      string userId = login.UserName;

                      AuthenticationManager.Login(userId, false);
                      return Redirect(ControllerContext.HttpContext.Request.RawUrl);
                    }
            }

            return View(login);
        }

I used below code in my CSHTML file to display the Claims available for the user.


@if (HttpContext.Current.User.Identity.IsAuthenticated)
{
    <h1>Current User is- @HttpContext.Current.User.Identity.Name</h1>

    foreach (var claim in (HttpContext.Current.User.Identity as ClaimsIdentity).Claims)
    {
        <h5>@(claim.Type + "-" + claim.Value)</h5>
    }
}
else
{
    <h1>Not authorized</h1>
}

Limitations

While running the code you will find that, HttpContext.User doesn’t comes as ClaimsPrincipal but Sitecore’s User class, which is due to the fact that Sitecore uses User class internally and if we pass ClaimsPrincipal it will not work. Below are the three way to achieve that.

  1. Derive the Sitecore’s Account base class from ClaimsPrincipal.
  2. Create an extension method for User class e.g. User.ToClaimsPrincipal(), which will expose the inner principal available as private field within User object
  3.         public static ClaimsPrincipal ToClaimsPrincipal(this User user)
            {
                var innerUserField = user.GetType().GetField("_innerUser", BindingFlags.NonPublic | BindingFlags.Instance);
                if (innerUserField != null)
                    return innerUserField.GetValue(user) as ClaimsPrincipal;
    
                return null;
            }
    
    

    After this, you can always call IPrincipal.IsInRole method to verify, if logged-in user is in a particular role or not.

    claimsPrincipal.IsInRole("Admin")
    
  4. Create an extension method which will read the WIF’s session security token from cookie to get the ClaimsPrincipal

I would prefer the 2nd option due to clean and small implementation.

Conclusion

Above code helps to make Sitecore work with ClaimsIdentity. As this code relies on WIF for most part, it open up the possibility of integrating with STS (passive/active), OpenId Connect or OAuth2 with little code and configuration change.

Advertisements

About cprakash

A developer, passionate about .Net, Architecture and Security.
This entry was posted in Security, Sitecore, Sitecore MVC and tagged , . Bookmark the permalink.

9 Responses to Sitecore with ClaimsIdentity

  1. Pingback: Sitecore extranet authentication with OpenIdConnect | cprakash

  2. Chandra, thanks for the very useful post. However, I have identified a significant configuration typo, the “forms” authentication provider is not listed as a provider, and several blocks of code that are either never invoked or else don’t actually do anything. You might want to revise or revisit the code as the configuration problem cost me about two hours yesterday as I’m new to SiteCore and have very little exposure to the API. Once again many thanks you really helped a lot.

    • cprakash says:

      Thanks for the constructive feedback Anthony. I will revisit the code/configuration to fix it.

      • Anthony Geoghegan says:

        No problem. Once again, really helpful post. It’s sad that Sitecore say they support >= 4.5 version of .NET when 4.5 includes WIF with claims based security out-of-the-box now.

      • cprakash says:

        Sitecore is unable to support due to the SitecoreIdentity & Accounts class directly inheriting from IIdentity & IPrincipal but .NET 4.5 has added ClaimsIdentity and ClaimsPrincipal one level down in hierarchy. I believe making this change will requires a massive amount of code change along with validating every corner of Sitecore due to Security being center of Sitecore content access. I am hopeful that Sitecore will start supporting this very soon along with other new feature of .NET.

  3. Mahesh Wagh says:

    Thanks for this great post Prakash. Are you able to put a link to the source code?

  4. weeder200 says:

    gr8 post on claims .. Do you have any recommended links on further reading on WIF and claims

  5. Steve says:

    Using Sitecore 8.1, ClaimsTransformer does not get called unless I uncomment
    context.User = transformedPrincipal;
    Thread.CurrentPrincipal = transformedPrincipal;
    within the ClaimsTransformationHttpModule and add
    to .

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s