Sitecore extranet authentication with OpenIdConnect

Beginning of this year, I wrote about how to make ClaimsIdentity work with Sitecore, after that I tried integrating Sitecore extranet authentication with OpenId Connect but had little trouble as I was using Owin based pipelines to perform the integration which obviously doesn’t work due to execution sequence of Sitecore processing.

Recently, I had a chance to look at it again and it turned out that it is much simpler to implement via plain code than Owin based OpenId Connect client, below steps talks about, how it can be achieved.

For implementation, you would need two application,

  1. Authentication Server
  2. Client

Authentication Server
I am using IdentityServer V3 as server to perform the authentication but it should work with any other provider without any issue. I will skip the server setup process as their documentation does that best than me, it’s available here. For this integration, I have configured a client in IdentityServer with following code.

public static class Clients
    {
        public static IEnumerable<Client> Get()
        {
            return new[]
            {
                new Client 
                {
                    Enabled = true,
                    ClientName = "Sitecore Client",
                    ClientId = "sitecore",
                    Flow = Flows.Implicit,

                    RedirectUris = new List<string>
                    {
                        "https://local-sitecore.com/SignInCallback"
                    },

                    AllowAccessToAllScopes = true
                },
            };
        }
    }

If you look at the above code, it is using implicit flow and configured client name is “sitecore” with redirect url is – https://local-sitecore.com/SignInCallback, this is the page where IdentityServer will redirect after successful login.

My IdentityServer is hosted under following url – https://local-sts.com/identity

Client
My client is a Sitecore application primarily developed in Mvc with controller rendering, I am skipping the Sitecore page design aspect as they are pretty standard as like any other controller rendering.

But before we proceed, there are some prerequisites for this integration.

Prerequisite
You would need an Authentication Provider and Authentication Helper which understand Claims on the similar line of Sitecore’s Membership Provider.

This implementation and relevant code is available here – https://cprakash.com/2015/02/02/sitecore-with-claimsidentity/

Implementation
The client implementation consist of following

  1. SignIn method- SignIn method is responsible to redirect the user to Authentication server to provider the user id and password and validate the credential.
  2. SignIn Callback method- Callback method is responsible to accept the request after successful login and validate & process the id token provided to get the user claims. Once client has access to user claims, it can create authentication cookie with security token.

Most of the code is already available as part of the IdentityServer Samples, for this implementation you need to look for specific example given here.

Code

    public class AccountController : Controller
    {
        public ActionResult SignIn()
        {
            var state = Guid.NewGuid().ToString("N");
            var nonce = Guid.NewGuid().ToString("N");
            var url = Constants.AuthorizeEndpoint +
                "?client_id=sitecore" +
                "&response_type=id_token" +
                "&scope=openid email" +
                "&redirect_uri=https://local-sitecore.com/SignInCallback" +
                "&response_mode=form_post" +
                "&state=" + state +
                "&nonce=" + nonce;

            SetTempCookie(state, nonce);
            return Redirect(url);
        }

        [HttpPost]
        public ActionResult SignInCallback()
        {
            var token = Request.Form["id_token"];
            var state = Request.Form["state"];
            var claims = ValidateIdentityToken(token, state);
            
            // Try login as Claim User
            SessionSecurityToken sessionToken;
            if (!FederatedAuthentication.SessionAuthenticationModule.TryReadSessionTokenFromCookie(out sessionToken))
            {
                var nameClaim = claims.Where(c => c.Type == ClaimTypes.Name).FirstOrDefault();
                if (nameClaim == null)
                    nameClaim = claims.Where(c => c.Type == ClaimTypes.NameIdentifier).FirstOrDefault();

                if (nameClaim != null)
                {
                    var tempClaims = claims.Where(c => c.Type != ClaimTypes.Name);
                    claims = (new Claim[] { new Claim(ClaimTypes.Name, Globalize(Context.Domain.Name, nameClaim.Value)) }).Concat(tempClaims);
                }
                
                var identity = new ClaimsIdentity(claims, "Forms", ClaimTypes.Name, ClaimTypes.Role);
                var principal = new ClaimsPrincipal(identity);
                var sessionSecurityToken = new SessionSecurityToken(principal);
                var sam = FederatedAuthentication.SessionAuthenticationModule;
                sam.WriteSessionTokenToCookie(sessionSecurityToken);
            }

            return Redirect("/Welcome");
        }

        private IEnumerable<Claim> ValidateIdentityToken(string token, string state)
        {
            var certString = "MIIDBTCCAfGgAwIBAgIQNQb+T2ncIrNA6cKvUA1GWTAJBgUrDgMCHQUAMBIxEDAOBgNVBAMTB0RldlJvb3QwHhcNMTAwMTIwMjIwMDAwWhcNMjAwMTIwMjIwMDAwWjAVMRMwEQYDVQQDEwppZHNydjN0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqnTksBdxOiOlsmRNd+mMS2M3o1IDpK4uAr0T4/YqO3zYHAGAWTwsq4ms+NWynqY5HaB4EThNxuq2GWC5JKpO1YirOrwS97B5x9LJyHXPsdJcSikEI9BxOkl6WLQ0UzPxHdYTLpR4/O+0ILAlXw8NU4+jB4AP8Sn9YGYJ5w0fLw5YmWioXeWvocz1wHrZdJPxS8XnqHXwMUozVzQj+x6daOv5FmrHU1r9/bbp0a1GLv4BbTtSh4kMyz1hXylho0EvPg5p9YIKStbNAW9eNWvv5R8HN7PPei21AsUqxekK0oW9jnEdHewckToX7x5zULWKwwZIksll0XnVczVgy7fCFwIDAQABo1wwWjATBgNVHSUEDDAKBggrBgEFBQcDATBDBgNVHQEEPDA6gBDSFgDaV+Q2d2191r6A38tBoRQwEjEQMA4GA1UEAxMHRGV2Um9vdIIQLFk7exPNg41NRNaeNu0I9jAJBgUrDgMCHQUAA4IBAQBUnMSZxY5xosMEW6Mz4WEAjNoNv2QvqNmk23RMZGMgr516ROeWS5D3RlTNyU8FkstNCC4maDM3E0Bi4bbzW3AwrpbluqtcyMN3Pivqdxx+zKWKiORJqqLIvN8CT1fVPxxXb/e9GOdaR8eXSmB0PgNUhM4IjgNkwBbvWC9F/lzvwjlQgciR7d4GfXPYsE1vf8tmdQaY8/PtdAkExmbrb9MihdggSoGXlELrPA91Yce+fiRcKY3rQlNWVd4DOoJ/cPXsXwry8pWjNCo5JD8Q+RQ5yZEy7YPoifwemLhTdsBz3hlZr28oCGJ3kbnpW0xGvQb3VHSTVVbeei0CfXoW6iz1";
            var cert = new X509Certificate2(System.Convert.FromBase64String(certString));
            var result = Request.Cookies["TempCookie"];
            if (result == null)
            {
                throw new InvalidOperationException("No temp cookie");
            }

            if (string.IsNullOrWhiteSpace(result.Values["state"]))
            {
                throw new InvalidOperationException("invalid state");
            }

            var parameters = new TokenValidationParameters
            {
                ValidAudience = "sitecore",
                ValidIssuer = Constants.BaseAddress,
                IssuerSigningToken = new X509SecurityToken(cert)
            };

            var handler = new JwtSecurityTokenHandler();
            SecurityToken jwt;
            var id = handler.ValidateToken(token, parameters, out jwt);
            if (id.FindFirst("nonce").Value != result.Values["nonce"])
            {
                throw new InvalidOperationException("Invalid nonce");
            }

            this.HttpContext.Response.Cookies.Remove("TempCookie");
            return id.Claims;
        }

        public ActionResult SignOut()
        {
            AuthenticationManager.Logout();
            return Redirect(Constants.LogoutEndpoint);
        }

        private void SetTempCookie(string state, string nonce)
        {
            var cookie = new HttpCookie("TempCookie");
            var tempId = new System.Collections.Specialized.NameValueCollection();
            cookie.Values.Add("state", state);
            cookie.Values.Add("nonce", nonce);
            this.HttpContext.Response.Cookies.Add(cookie);
        }

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

    public static class Constants
    {
        public const string BaseAddress = "https://local-sts.com/identity";
        
        public const string AuthorizeEndpoint = BaseAddress + "/connect/authorize";
        public const string LogoutEndpoint = BaseAddress + "/connect/endsession";
        public const string TokenEndpoint = BaseAddress + "/connect/token";
        public const string UserInfoEndpoint = BaseAddress + "/connect/userinfo";
        public const string IdentityTokenValidationEndpoint = BaseAddress + "/connect/identitytokenvalidation";
        public const string TokenRevocationEndpoint = BaseAddress + "/connect/revocation";

        public const string AspNetWebApiSampleApi = "https://local-sitecore.com/";
    }

Above code deviates from original code to make it working with Sitecore. Original code drops the temporary cookie for state and none validation with Owin based methods to encrypt the values and create cookie but above code uses plain cookie to do that, you can use FormAuthentication’s encrypt method to perform the encryption.

Original code uses ClaimTypes.NameIdentifier but I have to add ClaimTypes.Name manually so that SitecoreIdentity can get the user name from identity else it was coming as empty.

The above code is sufficient to make the entire authentication process working but if you have any challenge please reach out I would be happy to help.

Advertisements
Posted in Framework, Security, Sitecore, Sitecore MVC | Tagged , , , | 10 Comments

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.

Posted in Security, Sitecore, Sitecore MVC | Tagged , | 9 Comments

Sitecore MVC Form Post – Simplified

During Sitecore User Virtual Summit 2014 Kern Herskind presented Sitecore MVC implementation and discussed on various aspect of it. It took me to surprise during discussion on MVC Form Post when I found that my Form Post article was listed in that, he talked about the short coming of my approach which was a valid one due to various concepts involved in implementation.

A discussion with him made me look at other possibilities. I feel that my solution has little overhead in implementation and various part needs to be connected properly to make it work. I looked at below 3 approaches and all of them have pros and cons but 1st one seems promising, at least it has huge improvement over previous one and it reduces the various wire up point which I had in implementation.

Lets discuss these approaches in details and their pros and cons.

Page Post and execute GET request

The idea behind this method is to let execute the Form Post but not write on the output stream and rather hold the result in buffer and execute page once again by changing the request type as HTTP GET. Once it reaches the method which is equivalent to POST, instead of executing the action method it will assign the Form Post result from buffer, all other method will continue as it is.

  1. Process

    • Execute the Form Post but hold the result
    • Return PageContext.Current.PageView as view result to execute the page
    • Add POST method result instead of executing corresponding action method’s GET
  2. Pros

    • Seamlessly handle the form validation, success or redirects
    • Allow HTTP Verbs on the action method
  3. Cons

    • Semantically incorrect to call GET during POST processing
    • Both method name should be same but alternative is to specify the GET method as part of ImportMethodName property

ExportResult & ImportResult attribute

    public abstract class ResultTransfer : ActionFilterAttribute
    {
        protected static readonly string Key = typeof(ResultTransfer).FullName;
        protected static readonly string OriginalRequestTypeKey = Key + "_OriginalRequestType";

        protected virtual string CreateResultKey(ControllerContext ctrlContext, string actionMethodName)
        {
            return Key + "_" + ctrlContext.Controller.GetType().Name + "_" + actionMethodName;
        }
    }

    public class ExportResult : ResultTransfer
    {
        public string ImportMethodName { get; set; }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            //Don't Export if we are redirecting
            if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
                return;

            if (!filterContext.HttpContext.Request.RequestType.Equals("POST", System.StringComparison.OrdinalIgnoreCase))
                return;

            // Keep the result from post method
            var resultKey = this.CreateResultKey(filterContext, string.IsNullOrWhiteSpace(ImportMethodName) ? filterContext.ActionDescriptor.ActionName : ImportMethodName);
            filterContext.HttpContext.Items[resultKey] = filterContext.Result;

            // Start the GET request for page
            filterContext.HttpContext.Items[OriginalRequestTypeKey] = filterContext.HttpContext.Request.RequestType;
            this.SetHttpMethod("GET");
            
            IView pageView = PageContext.Current.PageView;
            filterContext.Result = new ViewResult { View = pageView };
            
            base.OnActionExecuted(filterContext);
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            // Restore the original request type
            var originalRequestType = filterContext.HttpContext.Items[OriginalRequestTypeKey] as string;
            if (!string.IsNullOrWhiteSpace(originalRequestType))
                this.SetHttpMethod(originalRequestType);

            base.OnResultExecuted(filterContext);
        }

        private void SetHttpMethod(string httpMethod)
        {
            var a = System.Web.HttpContext.Current.Request.GetType().GetField("_httpMethod", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            a.SetValue(System.Web.HttpContext.Current.Request, httpMethod);
            System.Web.HttpContext.Current.Request.RequestType = httpMethod;
        }
    }

    public class ImportResult : ResultTransfer
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (!filterContext.HttpContext.Request.RequestType.Equals("GET", System.StringComparison.OrdinalIgnoreCase))
                return;

            var requestItems = filterContext.HttpContext.Items;

            var resultKey = this.CreateResultKey(filterContext, filterContext.ActionDescriptor.ActionName);
            var originalRequestType = requestItems[OriginalRequestTypeKey] as string;

            if ("POST".Equals(originalRequestType, System.StringComparison.OrdinalIgnoreCase) && requestItems[resultKey] != null)
                filterContext.Result = requestItems[resultKey] as ActionResult;

            base.OnActionExecuting(filterContext);
        }
    }

Example



        [HttpGet]
        [ImportResult]
        public ActionResult Index()
        {
            var model = new SearchModel();
            return View(model);
        }

        [HttpPost]
        [ExportResult(ImportMethodName="Index")]
        public ActionResult Search(SearchModel search)
        {
            if (ModelState.IsValid)
            {
                search.Result = new List<string> { "Hello!", "Hi!!!" };
            }

            return View(search);
        }

Page Post without ExecuteFormHandler

This is same as Kevin B.’s solution and ValidateFormHandler will take care of all logic.

  1. Process

    • ExecuteFormHandler is not required because Page is handling the POST.
    • Decorate all POST method with ValidateFormHandler
    • Both action method name should be same
  2. Pros

    Handle scenarios like Form Validation, failure and success.

  3. Cons

    • Doesn’t allow HttpGet attribute on action method – this assumes that rendering will act safe during POST
    • Execute GET method during POST
    • POST should have same name as GET
public ActionResult Index()
{
 // return view;
}

[HttpPost, ValidateFormHandler]
public ActionResult Index()
{
  // return view;
}

Always redirect in Post – (Follow PRG)

  1. Process

    • Execute the FormPost pipeline
    • Always do redirect from POST method
    • Export ModelState for failure scenario, success scenario redirect with query string
    • In Get method add the model state to display failure
  2. Pros

    • Allows to define GET on action method
    • Can POST to a method which is not defined as rendering on Page.
  3. Cons

    • Decorate both action method with attributes
    • Always redirect including validation failure
    • If you want to retain the value then use Session or any other cache mechanism.
        [HttpGet]
        [ImportModelState]
        public ActionResult Index(string success)
        {
            var model = new ContactUsModel();
            if (!string.IsNullOrWhiteSpace(success) && success == "true")
            {
                model.Result = "You details has been recorded, we will contact you very soon.";
            }

            return View(model);
        }

        [HttpPost]
        [ExportModelState]
        public ActionResult Index(ContactUsModel model)
        {
            if (ModelState.IsValid)
            {
                // -To Do- Save the model data into data store
                return Redirect(ControllerContext.HttpContext.Request.RawUrl + "?success=true");
            }

            return Redirect(ControllerContext.HttpContext.Request.RawUrl);
        }

ImportModelState & ExportModelState Attribute

Here is the source code for Import/Export Attributes.


    public abstract class ModelStateTransfer : ActionFilterAttribute
    {
        protected static readonly string Key = typeof(ModelStateTransfer).FullName;
    }

    public class ExportModelState : ModelStateTransfer
    {
        public string ImportMethodName { get; set; }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            //Only export when ModelState is not valid
            if (!filterContext.Controller.ViewData.ModelState.IsValid)
            {
                //Export if we are redirecting
                if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
                {
                    ImportMethodName = string.IsNullOrWhiteSpace(ImportMethodName) ? filterContext.ActionDescriptor.ActionName : ImportMethodName;
                    var privateKey = Key + "_" + filterContext.Controller.GetType().Name + "_" + ImportMethodName;
                    filterContext.HttpContext.Session[privateKey] = filterContext.Controller.ViewData.ModelState;
                }
            }

            base.OnActionExecuted(filterContext);
        }
    }

    public class ImportModelState : ModelStateTransfer
    {
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            var privateKey = Key + "_" + filterContext.Controller.GetType().Name + "_" + filterContext.ActionDescriptor.ActionName;

            var modelState = filterContext.HttpContext.Session[privateKey] as ModelStateDictionary;
            if (modelState != null)
            {
                //Only Import if we are viewing
                if (filterContext.Result is ViewResult)
                {
                    filterContext.Controller.ViewData.ModelState.Merge(modelState);
                }
                
                //Otherwise remove it.
                filterContext.HttpContext.Session.Remove(privateKey);
            }

            base.OnActionExecuted(filterContext);
        }
    }

Summary
There are other implementation possible which I haven’t included them here and I was trying to simplify the earlier solution. Now usage of these solutions depends upon the problem and complexity of the application. All above options have some limitation and ideal PRG (POST->Redirect->GET) would have been to render the page as is for validation failure and redirect on success but Sitecore’s MVC implementation is little tricky to implement.

The best would be to Sitecore to come out with a standard process for implementation.

Posted in Sitecore, Sitecore MVC | Tagged | 4 Comments

Exception Handling in Sitecore MVC

This is 3rd article in series of Framework for Sitecore MVC development. Source code for this article is available on github.

  1. Pluggable Mvc Areas Implementation
  2. Form Post in Siteocre MVC
  3. Exception Handling
  4. Dependency Injection – Container agnostic
  5. Asynchronous Logging – Framework agnostic
  6. Integration with Claims Identity
  7. Glass Mapper Implementation
  8. Unit Testing

This article talks about exception handling for Sitecore Mvc in multi-site implementation and resolves other issues like sending correct http status code and customizing the behavior of MVC renderings.

  1. Common exception handling solution
  2. Multi-site exception handling
  3. Correct HTTP status code and content for error page
  4. Respect CustomError flag set in web.config file
  5. Customize behavior of MVC renderings in case of error

Available solution

John West has talked about different aspect of exception handling in Sitecore MVC, in his 4 blog series.

Handling Exceptions Globally in MVC Solutions Using the Sitecore ASP.NET CMS
Handling Rendering Exceptions in MVC Solutions with the Sitecore ASP.NET CMS
Handling Exceptions Globally in MVC Solutions Using the Sitecore ASP.NET CMS, Revisited
All About Exception Management with the Sitecore ASP.NET CMS

Thumb Rule

A general thumb rule of exception handling is to handle the exception which can be recovered and for all other exceptions, let the default application behavior handle it. Keep the exception handling as simple as possible to avoid failure in failover scenario. Also, with Sitecore, we use nested presentation layer where exception occurred in one of the rendering at Nth level might not be correctly represented once it bubble up to exception handler, so it is desirable to capture all possible information available from exception.

Couple resources for reference as best practice-

Best Practices for Exceptions
Exception Handling Best Practices in .NET

Sitecore MVC Life Cycle

David Morison from Sitecore has a nice diagram with details on Sitecore MVC execution life cycle. This diagram explains how Sitecore runs SitecoreController, various other pipelines to execute MVC controller action & view renderings and collect all generated output to render via response stream. In sort, Sitecore executes all controller/action defined on the page/presentation details one by one and collect the output html and send it to browser for display.

Common exception handling solution
This solution relies on a HTTP Module implementation to handle all application level exceptions and pass it on to a generic factory to determine the exception handler based on current executing site. It could be written inside the Global.asax file itself but my goal is to avoid any code in global.asax file and externalize all code to framework.

 public class ErrorHandlerModule : IHttpModule
    {
        public void Init(HttpApplication application)
        {
            application.Error += new EventHandler(OnError);
        }

        void OnError(object sender, EventArgs e)
        {
            var application = (HttpApplication)sender;
            var error = application.Server.GetLastError();
            var exceptionHandler = ExceptionHandlerFactory.Create();

            var context = new HttpContextWrapper(HttpContext.Current);

            // Custom error flag check 
            if (!context.IsCustomErrorEnabled)
                return;

            exceptionHandler.HandleException(error, context);
        }

        public void Dispose()
        {
        }
    }

I am using Microsoft.Web.Infrastructure library to register the HTTP Module but you can always register through web.config.

 Microsoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule(typeof(ErrorHandlerModule));

Multi-site exception handling

The other goal of this solution is to utilize and extend the customError section provided by default asp.net implementation to support error handling for multisite where different error pages are required for each web sites.

Currently, this solution has two exception handler, one for web site specific exceptions and relies on custom configuration to identify web site specific error page based on http status code and another one for global exceptions which uses web.config customError section configuration to display the error page. Both the exception handler derive from a base exception handler class with basic implementation.

Site Exception Handler –

public class SiteExceptionHandler : BaseExceptionHandler
    {
        public SiteExceptionHandler(string siteName)
        {
            if (string.IsNullOrWhiteSpace(siteName))
                throw new ArgumentNullException("siteName", "Site name is null.");

            this.siteName = siteName;
        }

        private readonly string siteName = string.Empty;

        public override string GetErrorUrl(int statusCode)
        {
            // Get Url for specific site.
            return GetSiteErrorUrl(statusCode);
        }

        private string GetSiteErrorUrl(int statusCode)
        {
            var statusError = CustomError.Instance.GetSiteErrors(siteName);
            return statusError.FirstOrDefaultStatusUrl(statusCode.ToString(CultureInfo.InvariantCulture));
        }
    }

Global Expcetion Handler –

public class GlobalExceptionHandler : BaseExceptionHandler
    {
        public GlobalExceptionHandler()
            : this(ConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection)
        {
        }

        public GlobalExceptionHandler(CustomErrorsSection errorSection)
            : this(SiteErrors.ConvertFrom(errorSection))
        {
        }

        public GlobalExceptionHandler(SiteErrors statusErrors)
        {
            this.statusErrors = statusErrors;
        }

        private readonly SiteErrors statusErrors;

        public override string GetErrorUrl(int statusCode)
        {
            return statusErrors.FirstOrDefaultStatusUrl(statusCode.ToString(CultureInfo.InvariantCulture));
        }
    }

Configuration –

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <sitesErrors>
      <site name="website">
        <errors defaultUrl="/500.aspx">
          <error statusCode="404" url="/404"/>
          <error statusCode="500" url="/500.aspx"/>
        </errors>
      </site>
      <site name="secure">
        <errors defaultUrl="/500.aspx">
          <error statusCode="404" url="/404"/>
          <error statusCode="500" url="/500.aspx"/>
        </errors>
      </site>
    </sitesErrors>
  </sitecore>
</configuration>

 <customErrors mode="RemoteOnly" defaultRedirect="/500.aspx">
      <error statusCode="404" redirect="/404"/>
      <error statusCode="500" redirect="/500.aspx"/>
 </customErrors>
 

Following are the assumption behind the configuration.
Page not found (404) Error – This page can be configured within Sitecore on per site basis.
Unauthorized (401) Error – This page can be configured within Sitecore on per site basis.
Server side (500) Error – As this is a generic error page for any error happening in the application it should be external aspx or html page to avoid any dependency on Sitecore.

Correct HTTP status code and content for error page

Most of the implementation perform Response.Redirect to navigate to error page while handling exception, but this result in sending status code 302 for redirect and 200 for successful rendering of error page itself. In this case browser fails to identify error on server side and treat it as just redirect. To fix this, base exception handler uses Server.TransferRequest to process request with error page, in-order to emit correct status code, I am writing status code directly in ASPX page.

The base class-

public abstract class BaseExceptionHandler
    {
        public abstract string GetErrorUrl(int statusCode);

        public virtual void HandleException(Exception exception, HttpContextBase httpContext)
        {
            if (httpContext == null)
                return;

            if (!httpContext.IsCustomErrorEnabled)
                return;

            if (httpContext.Response.IsRequestBeingRedirected)
                return;

            var httpException = exception as HttpException;
            var statusCode = (int)System.Net.HttpStatusCode.InternalServerError;
            
            if (httpException != null)
                statusCode = httpException.GetHttpCode();

            var redirectUrl = GetErrorUrl(statusCode);

            if (string.IsNullOrWhiteSpace(redirectUrl))
                throw new NullReferenceException("Error Url for status code is null.");

            httpContext.Server.TransferRequest(redirectUrl, true);
        }
    }

MVC Rendering Errors

Exception in Controller/Action execution can be handled by two ways.

  1. with ExecuteRenderer-
    If you are trying to capture any Mvc specific exception then this is the place to add the logic, it capture all Mvc exception including controller not found or Mvc pipeline errors.
    The drawback with this pipeline is that if you want to terminate the execution of the pipeline like any other normal Sitecore pipeline, it doesn’t help much because of pipeline renewing the RenderRenderingArgs argument, and not respecting the AbortPipeline method call which doesn’t terminate the pipeline while executing the MVC request.
  2. with ExceptionProcessor-

    Sitecore adds an ExceptionProcessor pipeline “ShowAspNetErrorMessage” in MVC life cycle to handle MVC related errors. By default this processor doesn’t break the page but displays the error on the page by replacing the output of the rendering, which is good during development but not for live site. You can assume it as .NET YSOD per renderings.

    This processor handles errors during MVC Action method or renderings execution, any failure during Action method execution or view execution will be caught by this processor and handled appropriately. As, this exception is raised on per rendering execution basis, it’s very easy to customize the behavior of individual renderings.

    using System.Web;
    using Framework.Sc.Extensions.ErrorHandler;
    using Sitecore.Mvc.Pipelines.MvcEvents.Exception;
    using System.Web.Mvc;
    
    namespace Framework.Sc.Extensions.Pipelines.MvcEvents
    {
        public class RenderingExceptionProcessor : ExceptionProcessor
        {
            public bool CustomRenderingBehaviourRequired { get; set; }
    
            public override void Process(ExceptionArgs args)
            {
                HandleError(args.ExceptionContext);
            }
    
            protected virtual void HandleError(ExceptionContext exceptionContext)
            {
                var httpContext = exceptionContext.HttpContext;
    
                // Bail if we can't do anything; propogate the error for further processing. 
                if (httpContext == null)
                    return;
                if (!CustomRenderingBehaviourRequired)
                    return;
                if (exceptionContext.ExceptionHandled)
                    return;
                if (httpContext.IsCustomErrorEnabled)
                    exceptionContext.ExceptionHandled = true;
    
                var innerException = exceptionContext.Exception;
                
                // - Todo - custom error handling for rendering.
                // ExceptionHandlerFactory.Create().HandleException(innerException, httpContext);
            }
        }
    }
    

    Current implementation checks for a flag ‘CustomRenderingBehaviourRequeired’ and if it is off, it will let error be handled as global error. For custom behavior, I have a concept demonstrated here – Custom Error Handling in MVC, this solution uses a content service to pull the configured error message for rendering in case of error but a possible drawback is that it may not appear in context of page look and feel.

    An alternative could be to create a data template which can accept the rendering behavior i.e.(Redirect, hide rendering or alternative error rendering) and if it supposed to display error rendering then a field to accept alternative view rendering path. As error rendering is tied to a specific rendering it might result into too many renderings, to avoid that data source of the error rendering should be generic enough to allow specific error messages and control the look and feel of the rendering by configuring CSS classes etc.

    Above behavior can be accomplished by below code.

    1. Hide Rendering- If a rendering fail to execute then just hide the rendering, it can be done via simple code.
      exceptionContext.Result = new EmptyResult();
      
    2. Alternative Rendering- This allows a rendering failure to be handled gracefully and an alternative rendering to be displayed, alternative rendering could be as simple as a Promo which doesn’t have any functionality but just an image.
      exceptionContext.Result = new ViewResult(); // and specify the view to be displayed.
      

Handle Page Not Found (404) Errors

Page not found (404) is handled via two pipeline which register themselves in HttpBeginRequest and HttpRequestProcessed pipeline.

1st pipeline executes after ItemResolver and check if requested url has a corresponding item in sitecore content tree, if it is not then it will load the sitecore item configured for 404 scenario.

2nd pipeline executes after request processing is complete to set the 404 status code.

<httpRequestBegin>
        <processor type="Framework.Sc.Extensions.Pipelines.HttpRequest.PageNotFoundHandler, Framework.Sc.Extensions" patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel']"/>
</httpRequestBegin>
<httpRequestProcessed>
        <processor type="Framework.Sc.Extensions.Pipelines.HttpRequest.SetNotFoundStatusCode, Framework.Sc.Extensions" />
</httpRequestProcessed>

Code for this article is available on GitHub.

Posted in Sitecore, Sitecore MVC | Tagged | 1 Comment

Sitecore 8: Fix MVC area execution and registration

I am upgrading my Sitecore Mvc Framework to Sitecore 8 and it broke due to two changes in Sitecore 8.

  1. Tag Injection Apps introduced in Sitecore 7.5
  2. Social Client area introduced in Sitecore 8

Both applications are designed as MVC Areas in Sitecore 8 and they individually try to register themselves as MVC areas in different pipeline.

Issue#1
If you are using Sitecore with multiple MVC Areas then this post might help you to resolve this issue. Most of the solution (example below) like my solution insert ‘area’ key and it’s value in route data token to let MVC infrastructure identify the correct area and create the controller instance to execute the action method but after execution during cleanup we set the ‘area’ value as null for consistency.

e.g.
Sitecore MvcContrib
Sitecore MVC in a multisite environment: area’s

Sitecore 7.5 introduced Tag Injection Apps designed in MVC and it has specific implementation of ControllerFactory (TagInjectionControllerFactory) and IDependencyResolver (TagInjectionDependencyResolver), this custom TagInjectionControllerFactory implementation checks if controller request is for AppCenter area before passing it to MVC’s implementation. While comparing the area value it throws object null exception if area value is null.

Sitecore 7.5 Method in question Class – TagInjectionControllerFactory

 public bool CanHandle(RequestContext requestContext)
 {
 if (!requestContext.RouteData.DataTokens.ContainsKey("area"))
 {
 return false;
 }
 return string.Equals(requestContext.RouteData.DataTokens["area"].ToString(), this.areaName, StringComparison.InvariantCultureIgnoreCase);
 }

In order to avoid this issue it’s better to set the route data token value to empty string or better remove it.

 protected override void ExecuteController(Controller controller)
        {
            RequestContext requestContext = PageContext.Current.RequestContext;
            var value = requestContext.RouteData.Values["controller"];
            var value2 = requestContext.RouteData.Values["action"];
            var value3 = requestContext.RouteData.DataTokens["area"];
            var value4 = requestContext.RouteData.DataTokens["namespace"];

            try
            {
                requestContext.RouteData.Values["controller"] = ActualControllerName;
                requestContext.RouteData.Values["action"] = ActionName;
                requestContext.RouteData.DataTokens["area"] = Area;

                var namespaces = new[] { string.Empty };
                if (!string.IsNullOrWhiteSpace(Namespace))
                    namespaces = Namespace.Split(new[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries);

                requestContext.RouteData.DataTokens["namespace"] = namespaces;
                ((IController)controller).Execute(PageContext.Current.RequestContext);
            }
            finally
            {
                requestContext.RouteData.Values["controller"] = value;
                requestContext.RouteData.Values["action"] = value2;
                
                // Perform cleanup to avoid any issues
                if (value3 != null)
                    requestContext.RouteData.DataTokens["area"] = value3;
                else
                    requestContext.RouteData.DataTokens.Remove("area");

                if (value4 != null)
                    requestContext.RouteData.DataTokens["namespace"] = value4;
                else
                    requestContext.RouteData.DataTokens.Remove("namespace");
            }
        }

Issue#2

This issue is more to my solution specific where I am using WebActivatorEx.PostApplicationStartMethod to register all the dependencies including MVC Area Registration avoid hard binding with Application Start event. With Sitecore 8, Sitecore has introduced a new area for Social client which has its own pipeline which hooks into Sitecore’s Initialize pipeline and register the ‘Social’ area. As my code was executing after initialize pipeline has executed it was throwing below exception.

Server Error in '/' Application.

A route named 'SocialConnectorSpecific' is already in the route collection. Route names must be unique.
Parameter name: name 
  Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 

 Exception Details: System.ArgumentException: A route named 'SocialConnectorSpecific' is already in the route collection. Route names must be unique.
Parameter name: name

Source Error: 

Line 24:         public static void Start()
Line 25:         {
Line 26:             AreaRegistration.RegisterAllAreas();
Line 27:             IoC.InitializeWith(new DependencyContainerFactory());
Line 28:             TypeRegistration.RegisterTypes();

Stack Trace: 
[ArgumentException: A route named 'SocialConnectorSpecific' is already in the route collection. Route names must be unique.
Parameter name: name]
   System.Web.Routing.RouteCollection.Add(String name, RouteBase item) +2445958
   System.Web.Mvc.RouteCollectionExtensions.MapRoute(RouteCollection routes, String name, String url, Object defaults, Object constraints, String[] namespaces) +394
   System.Web.Mvc.AreaRegistrationContext.MapRoute(String name, String url, Object defaults, Object constraints, String[] namespaces) +147
   System.Web.Mvc.AreaRegistrationContext.MapRoute(String name, String url, Object defaults, Object constraints) +53
   Sitecore.Social.Client.Mvc.Areas.Social.SocialAreaRegistration.RegisterArea(AreaRegistrationContext context) +154
   System.Web.Mvc.AreaRegistration.CreateContextAndRegister(RouteCollection routes, Object state) +196
   System.Web.Mvc.AreaRegistration.RegisterAllAreas(RouteCollection routes, IBuildManager buildManager, Object state) +238
   System.Web.Mvc.AreaRegistration.RegisterAllAreas(Object state) +75
   System.Web.Mvc.AreaRegistration.RegisterAllAreas() +24

As, I am trying to avoid any code in Global.asax or Web Application for initialization and writing outside in a bootstrap assembly, I created a pipeline which will hook into Sitecore’s initialize method and call AreaRegistration and all other application initialization code.

<initialize>
        <processor type="Framework.Bootstrap.Start.Bootstrapper, Framework.Bootstrap" patch:before="processor[@type='Sitecore.Pipelines.Loader.ShowVersion, Sitecore.Kernel']" />
      </initialize>

Pipeline –

[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(Framework.Bootstrap.Start.Bootstrapper), "Initialize")]
[assembly: WebActivatorEx.ApplicationShutdownMethod(typeof(Framework.Bootstrap.Start.Bootstrapper), "Shutdown")]

namespace Framework.Bootstrap.Start
{
    public class Bootstrapper
    {
        public static void Initialize()
        {
            // Register our modules
            Microsoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule(typeof(ApplicationErrorModule));
            //DynamicModuleUtility.RegisterModule(typeof(ClaimsTransformationHttpModule));
        }

        public static void Start()
        {
            AreaRegistration.RegisterAllAreas();
            IoC.InitializeWith(new DependencyContainerFactory());
            TypeRegistration.RegisterTypes();
            DependencyResolver.SetResolver(new CustomDependencyResolver());
            ValueProviderFactories.Factories.Add(new TempDataModelProviderFactory());
        }

        public static void Shutdown()
        {
        }

        public virtual void Process(object args)
        {
            Start();
        }
    }
}
Posted in Sitecore MVC | Tagged | 3 Comments

Form Post in Sitecore MVC

Update – 01/12/2015- There is a simplified version available here — https://cprakash.com/2015/01/12/sitecore-mvc-form-post-simplified/

This post is 2nd in series of the proposed framework to solve some of the common problem in Sitecore MVC implementation. In this post I am going to talk about the problem with Form Post in Sitecore MVC and how this framework helps to solve it.

There are some talk already in different forums on this topic and it is good to link it here for reference.

Challenges with current implementation

  1. Sitecore MVC is not ASP.NET MVC – Yes, Sitecore works differently in terms of MVC, behind the scene it runs a Sitecore controller but for Form Post it simply runs a ExecuteFormHandler pipeline and emit out the response generated by controller/action, which may not be ok, if you are looking for the full page again.
  2. Form Validation– If your server side model validation fails, it can not get you back to the page with nice error message as like ASP.NET MVC.
  3. BeginForm is not supported- If you try to use ASP.NET MVC’s BeginForm it will try posting to current page which result into calling Post action on all the available renderings.
  4. Possible Ajax solution- Yes, it’s a good solution but this gives you additional task of handling JS logic.

As Sitecore executes form post in Request Begin pipeline, it is not possible to get the same page back again unless we execute the Sitecore’s page rendering pipeline, which become tedious to handle. This prompted me to explore the other possibility of solving this problem and the solution resulted into bunch of code but most of it is abstract.

Solution
PRG (Post-Redirect-GET) to rescue, in fact it is a better way to handle the Form Post, it avoids accidental double post and page refresh issues. This solution is based on PRG and uses some of the code from Kazi Manzur‘s MVC Best Practices blog (#13).

How it works
Here is how this solution works.

  1. Extend the SessionStateTempDataProvider to hold temp data values for request life time and if SitecoreController is executing then keep the temp data value in request’s items object as this data could belongs to one of the controller in execution later.
  2. On Form Post execution preserve the model state into extended TempData provider, which can be retrieved back on execution after redirect.
  3. After Form Post execution Redirect to current/new page based on the functionality required. You might need result of the form execution, if yes, store that as well.
  4. In GET action method, use TempDataModelBinder to retrieve the Model from TempData and initialize the Model object.
  5. In GET execution, import the model state and return along with view. At this point if you want to make a decision based on GET/POST request, you need additional flag to identify if model is return from Post or not.

Form Post Flow

Let the code talk

Lets build a sample to see, how it look like.

The controller action method for GET request

The Index action method is dummy and renders the view to accept the input. There is one flag with the model which suggest if the model resulted out of PRG or not. TempDataModelBinder binds the model after PRG and if this is a brand new GET request it will get initialized as like normal MVC. Action method is decorated with ImportModelStateFromTempData attribute which will execute OnActionExecuted method and this achieves the job of sending the ModelState object to view after PRG, if any validation errors are available.

        [HttpGet]
        [ImportModelStateFromTempData]
        public ActionResult Index([TempDataModelBinder]ContactUsModel model)
        {
            if (!model.IsPost)
            {
                // Todo- if something is required during get on form post
            }

            return View(model);
        }

Action method generates the View looks like below, it slightly differs from normal MVC views due to usage of @Html.Sitecore().AreaFormHandler() extension helper method which is used to specify the Area/Controller/Action/Namespace which will be used by ExecuteAreaFormHandler pipeline to handle the POST request. It is an extension on top of @Html.Sitecore().FormHandler(). Below example assume you are supplying the parameters manually to AreaFormHander method, but you can use another implementation which pulls up these values from AreaControllerRendering fields. See the definition below.

Area Controller Rendering Template

Current view support both BeginForm and BeginRouteForm with Sitecore Route.

@using Framework.Sc.Extensions.Helpers
@model Common.Models.ContactUsModel

@*@using (Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName, FormMethod.Post, new { role = "form" }))*@
@using(Html.BeginForm())
{
    @*@Html.Sitecore().AreaFormHandler()*@
    @Html.Sitecore().AreaFormHandler("ContactUs", "ContactUsSave", "Common", "Common.Controllers")

    <div class="form-group">
        @Html.LabelFor(m => m.Name, new { @class = "control-label" })
        @Html.EditorFor(m => m.Name, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.Name)
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Email, new { @class = "control-label" })
        @Html.EditorFor(m => m.Email, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.Email)
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.PhoneNumber, new { @class = "control-label" })
        @Html.EditorFor(m => m.PhoneNumber, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.PhoneNumber)
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
    <div>@Model.Result</div>
}

The Action Method for POST request

The POST action method is pretty much same like normal ones except on 2 points, first one being ExportModelStateToTempData which will fire OnActionExecuted method and store ModelState in TempData to get it back during GET request in PRG. Second one is the Redirect extension method, which accept two parameter, Url (if specified it will redirect to that url else it will redirect to current url) and model object containing result after the method execution.

        [HttpPost]
        [ExportModelStateToTempData]
        public ActionResult ContactUsSave(ContactUsModel model)
        {
            if (ModelState.IsValid)
            {
                model.Result = "You details has been recorded, we will contact you very soon.";
            }

            return this.Redirect(model);
        }
    

Source code for this solution and examples are posted on GitHub.

What you can do

  1. BeginForm- Yes, you can use it but can not specify controller/action name and let it decide about the POST Url, and make sure you are using AreaFormHandler/FormHandler to let Sitecore know which controller/action to execute for processing.
  2. Form Validation- Yes, Action Filter will store ModelState in temp data during POST and transfer it to GET method to render in UI. Additional validation message can be added in POST method.
  3. Multiple Form- Yes, as you are using AreaFormHandler or FormHandler, it will send the POST to appropriate controller/action method.
  4. Redirect- As this solution is based on PRG pattern, you have to always redirect after POST.
  5. TempData- Though this solution makes TempData available for complete request, but as in Sitecore Mvc we don’t use TempData so I don’t expect anyone to use it. Currently, it is not tested fully and behavior is unknown; Use it at your own risk.

Disclaimer-

I have tested this solution with 2 simple form on the page with both variation of redirect, BeginForm/BeginRouteForm and AreaFormHandler and it works without any problem. If you have any issue drop me a mail or post your comment.

Posted in Framework, MVC, Sitecore, Sitecore MVC | Tagged , , | 4 Comments

Sitecore with MVC Areas as pluggable Module

This post is first in series to describe the features provided by framework for Sitecore Mvc. For complete list and detail please see main post.

I have been working on Sitecore Mvc since its Technical Preview version and we were the first few early adopter of Mvc version. My Team had good or I would say rather better architecture laid out to handle Sitecore, Mvc areas, Helper library, services and other layers in the solution. I moved on but need of similar framework comes again and again. During this entire duration I have seen several post on Sitecore with areas implementation on SO and elsewhere.

Pluggable module concept is based on approach suggested by Sean Kearney back in 2010 and I believe it should be the standard model for Sitecore development as it reduces complexity and keeps the code clean from other dependencies. On the other hand Sitecore’s support for Mvc areas is limited and it requires hard coding of view path. Kevin Buckley came out with additional package to support the Mvc areas and solution described in this post is build on top of it.

Pluggable area has helped me to run an isolated instance of site with lower possible combination of functionality, spinning up new site with only certain section of the application, resolving performance issue of the application by isolating the section of the site.

Sitecore Mvc Execution cycle

David Morrison has posted the block diagram giving the details how Sitecore executes the Mvc Controller. Sitecore uses GetControllerRenderer processor pipeline to emit the response after controller execution. This pipeline reads Controller & Action name from controller rendering definition and hands over to ControllerRenderer which intern calls ControllerRunner to execute the controller’s action method and emit the response.

Solution

As Sitecore still uses underline Mvc infrastructure to create the controller it is easy to inject the area name to build the support for Mvc Areas. To avoid conflict between duplicate controller names it make sense to inject namespace name also in route data. To build the pluggable Mvc areas support this solution has 2 major component.

  1. Pluggable Mvc Areas
  2. Build support for Sitecore
    1. Sitecore Template
    2. Code/Configuration

Pluggable MVC Area

The built-in Project template in Visual studio allows areas to be added as part of the web project. But if you are designing the enterprise project with several dependencies and large team it becomes impossible to have everything in one single web project, this can be avoided by creating separate web project for every areas. The only caveat to this approach is to have a class with AreaRegistration implementation.

Area Project Solution structure

This approach requires two changes.

  1. Deploy the Area project DLL to Host application bin folder
  2. Copy the Area project View files to Host application’s area folder

Deploy the Area project DLL to Host application bin folder

After build Area project DLL can be dropped inside the host application’s bin folder either using post build script or by setting the build output path (Project Properties->Build->Output->Output Path) to host app’s bin folder. On call of AreaRegistration.RegisterAllAreas() Mvc’s BuildManager class scan for all the assembly and looks for AreaRegistration implementation, if it is available then area route will get registered in the route table.

This approach is just for PoC and it can be very easily changed to nuget package during deployment.

Build Output Path

Update 10-30-2014

The above approach resulted me loosing the intellisense in my view files and I decided to copy the DLLs as well via XCOPY script. In fact, it’s much better as I can do a web publish of my area project and drop the files anywhere.

xcopy "$(TargetDir)*.*" "$(SolutionDir)host\bin\" /s /i /y

Copy the Area project View files to Host application’s area folder

This is required for Mvc to locate the View files and I am currently using post build xcopy to copy the file to Host App’s area folder.

xcopy “$(ProjectDir)Views” “$(SolutionDir)Host\Areas\$(ProjectName)\Views\” /s /i /y

Post Build XCopy

Sitecore Template

To hold the Area and Namespace name with controller rendering, I have created a new template which accepts Area Name and Namespace name.

Area Controller Rendering Template

Just remember the Template id of new Form template which is required to be passed to Sitecore via configuration while executing the controller renderer. It has Form Section as well on the template and that is required for Mvc Form Post support but it is not mandatory. Now assign Sitecore’s Controller Rendering as Base Template for this template.

Area Controller Rendering Template with Base Template

You can define the Insert option on Rendering folder for newly created template to allow creation of renderings quickly without referring the template again.

Area Controller Rendering as Insert option on rendering folders

Code/Configuration

Below are the classes which requires the extension

AreaControllerRendererProcessor

This class is responsible to create the instance of Area Controller Renderer by passing the controller, action, area and namespace name.

public class AreaControllerRendererProcessor : GetRendererProcessor
    {
        public virtual string TemplateId { get; set; }

        public override void Process(GetRendererArgs args)
        {
            if (args.Result != null)
            {
                return;
            }
            
            Template renderingTemplate = args.RenderingTemplate;
            if (renderingTemplate == null)
            {
                return;
            }
            if (!renderingTemplate.DescendsFromOrEquals(new ID(TemplateId)))
            {
                return;
            }
            args.Result = this.GetRenderer(args.Rendering, args);
        }

        protected virtual Renderer GetRenderer(Rendering rendering, GetRendererArgs args)
        {
            string action = rendering.RenderingItem.InnerItem.Fields["controller action"].GetValue(true);
            string controller = rendering.RenderingItem.InnerItem.Fields["controller"].GetValue(true);
            string area = rendering.RenderingItem.InnerItem.Fields["area"].GetValue(true);
            string namespaceNames = rendering.RenderingItem.InnerItem.Fields["namespace"].GetValue(true);

            return new AreaControllerRenderer
            {
                Action = action,
                Controller = controller,
                Area = area,
                Namespaces = namespaceNames
            };
        }
    }

AreaControllerRenderer

This class is responsible to call the ControllerRunner class by passing controller, action, area, and namespace and retrieve the result to write on response stream.

 public class AreaControllerRenderer : Renderer
    {
        public string Action { get; set; }
        public string Controller { get; set; }
        public string Area { get; set; }
        public string Namespaces { get; set; }

        public override string CacheKey
        {
            get
            {
                return "areacontroller::" + Controller + "#" + this.Action + "#" + Area + "#" + Namespaces;
            }
        }

        public override void Render(System.IO.TextWriter writer)
        {
            AreaControllerRunner controllerRunner = new AreaControllerRunner(this.Controller, this.Action, this.Area, this.Namespaces);

            string value = controllerRunner.Execute();
            if (string.IsNullOrEmpty(value))
            {
                return;
            }
            writer.Write(value);
        }

        public override string ToString()
        {
            return "Controller: {0}. Action: {1}. Area {2}. Namespaces {3}".FormatWith(new object[]
			{
				this.Controller,
				this.Action,
                this.Area,
                this.Namespaces
			});
        }
    }

AreaControllerRunner

Responsibility of this class is to assign the route data values before executing the controller to create the correct instance and execute it. After execution is finished restore the original values back in route data.

public class AreaControllerRunner : ControllerRunner
    {
        public AreaControllerRunner(string controllerName, string actionName, string area, string namespaceNames)
            : base(controllerName, actionName)
        {
            this.Area = area;
            this.ControllerName = controllerName;
            this.ActionName = actionName;
            this.Namespaces = namespaceNames;
        }

        public string Area { get; set; }
        public string Namespaces { get; set; }

        protected override void ExecuteController(System.Web.Mvc.Controller controller)
        {
            RequestContext requestContext = PageContext.Current.RequestContext;
            object value = requestContext.RouteData.Values["controller"];
            object value2 = requestContext.RouteData.Values["action"];
            object value3 = requestContext.RouteData.DataTokens["area"];
            object value4 = requestContext.RouteData.DataTokens["namespace"];

            try
            {
                requestContext.RouteData.Values["controller"] = this.ActualControllerName;
                requestContext.RouteData.Values["action"] = this.ActionName;
                requestContext.RouteData.DataTokens["area"] = this.Area;
                requestContext.RouteData.DataTokens["namespace"] = this.Namespaces.Split(new char[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries);
                ((IController)controller).Execute(PageContext.Current.RequestContext);
            }
            finally
            {
                requestContext.RouteData.Values["controller"] = value;
                requestContext.RouteData.Values["action"] = value2;
                requestContext.RouteData.DataTokens["area"] = value3;
                requestContext.RouteData.DataTokens["namespace"] = value4;
            }
        }
    }

Once everything is ready, plug it in with configuration in Sitecore execution pipeline.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.getRenderer>
        <processor patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.GetRenderer.GetViewRenderer, Sitecore.Mvc']" type="Framework.Sitecore.Extensions.MvcAreas.Pipelines.GetRenderer.AreaControllerRendererProcessor, Framework.Sitecore.Extensions" >
          <templateId>{D7018B2D-AE1F-47C0-B2ED-AC5BAFFB3C27}</templateId>
        </processor>
      </mvc.getRenderer>
    </pipelines>
  </sitecore>
</configuration>

That’s all is required to support the pluggable areas in Sitecore Mvc. Any feedback please drop a comment or mail me.

Posted in Sitecore | Tagged , , | 7 Comments