Thursday, November 5, 2015

How to fix the roles in EPiServer when using ASP.NET Identity



If you go into admin in EPiServer and then "Set Access Rights for", you will fail to choose your Identity 2.0 roles. Reason for this is that the Access Rights GUI in EPiServer are closely connected to the old membership provider, which you disable when you implement the ASP.NET Identity.

After a short brainstorm with my colleague Roger Nesheim, it became quite clear that the quickest way to fix this legacy problem was to use Dependency Injection and Inversion of Control. StructureMap is a DI/IOC tool exclusively for .NET programming, which we will use in this EPiServer projects. It helps in achieving looser coupling among classes and their dependencies.

Edit:
As Per suggested in the comment field, it's also possible to to implement this using built in provider instead of using StructureMap. So by add the following code in web.config and add the IdentitySecurityEntityProvider.cs to the project it will also work. Thanks :-)


  <episerver.framework>
    <securityEntity>
      <providers>
        <add name="IdentityProvider" type="TestSite.Business.AccessRights.IdentitySecurityEntityProvider, TestSite" />
      </providers>
    </securityEntity>
    <appData basePath="App_Data" />
    <scanAssembly forceBinFolderScan="true" />
    <virtualRoles addClaims="true">
      <providers>

Solution

First you need to setup the DI/IOC to override the “SecurityEntityProvider” and replace it with “IdentitySecurityEntityProvider” class. The setup below show how to do that.

ServiceLocationConfiguration.cs


using System;
using EPiServer.Security;
using StructureMap;
using TestSite.Business.AccessRights;
 
namespace TestSite.Business.ServiceLocation
{
    public static class ServiceLocationConfiguration
    {
        public static Action<ConfigurationExpression> Current
        {
            get
            {
                return configuration =>
                {
                    configuration.For<SecurityEntityProvider>().Use(() => new IdentitySecurityEntityProvider());
                };
            }
        }
    }
}

DependenciesInitializationModule.cs


using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
 
namespace TestSite.Business.ServiceLocation
{
    [InitializableModule]
    [ModuleDependency(typeof(ServiceContainerInitialization))]
    public class DependenciesInitializationModule : IConfigurableModule
    {
        public void Initialize(InitializationEngine context)
        {
        }
        public void Preload(string[] parameters) { }
        public void Uninitialize(InitializationEngine context)
        {
            //Add uninitialization logic
        }
        public void ConfigureContainer(ServiceConfigurationContext context)
        {
            context.Container.Configure(ServiceLocationConfiguration.Current);
        }
    }
}

Then you will need to override the different Search functions with new code to handle the communication with the ASP.NET Identity database. When this is in place the access rights with users and group will work as it used to.
Disclaimer: The code isn't tested on large database with a lot of users and groups.

IdentitySecurityEntityProvider.cs


using System;
using System.Collections.Generic;
using System.Linq;
using EPiServer.Security;
using TestSite.Models.Account;
 
namespace TestSite.Business.AccessRights
{
  public class IdentitySecurityEntityProvider : PagingSupportingSecurityEntityProvider
  {
    private readonly ApplicationDbContext _db = new ApplicationDbContext();
 
    public override IEnumerable<string> GetRolesForUser(string userName)
    {
        var theUser = _db.Users.FirstOrDefault(u => u.UserName.Equals(userName, StringComparison.CurrentCultureIgnoreCase));
        return theUser == null ? Enumerable.Empty<string>() : theUser.Roles.Select(role => _db.Roles.Find(role.RoleId).Name.ToString()).AsEnumerable();
    }
 
    public override IEnumerable<SecurityEntity> Search(string partOfValue, string claimType)
    {
      int totalCount;
      return this.Search(partOfValue, claimType, 0, int.MaxValue, out totalCount);
    }
 
    public override IEnumerable<SecurityEntity> Search(string partOfValue, string claimType, int startIndex, int maxRows, out int totalCount)
    {
      totalCount = 0;
      switch (claimType)
      {
        case "http://schemas.microsoft.com/ws/2008/06/identity/claims/role":
          return this.SearchRoles(partOfValue, startIndex, maxRows, out totalCount);
        case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name":
        case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier":
          return this.SearchUsers(partOfValue, startIndex, maxRows, out totalCount);
        case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress":
          if (!string.IsNullOrEmpty(partOfValue))
              return this.SearchEmails(partOfValue, startIndex, maxRows, out totalCount);
          else
              return Enumerable.Empty<SecurityEntity>();
        default:
          return Enumerable.Empty<SecurityEntity>();
      }
    }
 
    private IEnumerable<SecurityEntity> SearchEmails(string partOfName, int startIndex, int maxRows, out int totalCount)
    {
        var usersList = _db.Users.ToList();
        var users = usersList.Where(a => a.Email != null && a.Email.ToLower().Contains(partOfName.ToLower())).ToList();
        var entityList = users.Select(user => new SecurityEntity(user.Email)).ToList();
        totalCount = entityList.Count;
        return entityList;
    }
 
    private IEnumerable<SecurityEntity> SearchUsers(string partOfName, int startIndex, int maxRows, out int totalCount)
    {
        totalCount = 0;
        if (_db.Users == null) return Enumerable.Empty<SecurityEntity>();
        var usersList = _db.Users.ToList();
        var entityList = !string.IsNullOrEmpty(partOfName) ? usersList.Where(a => a.UserName.ToLower().Contains(partOfName.ToLower())).ToList().Select(user => new SecurityEntity(user.UserName)).ToList() : usersList.Select(user => new SecurityEntity(user.UserName)).ToList();
        totalCount = entityList.Count;
        return entityList;
    }
 
    private IEnumerable<SecurityEntity> SearchRoles(string partOfName, int startIndex, int maxRows, out int totalCount)
    {
        totalCount = 0;
        if (_db.Roles == null) return Enumerable.Empty<SecurityEntity>();
        var rolesList = _db.Roles.ToList();
        var roles = rolesList.Where(a => a.Name.ToLower().Contains(partOfName.ToLower())).ToList();
        var entityList = roles.Select(role => new SecurityEntity(role.Name)).ToList();
        totalCount = entityList.Count;
        return entityList;
    }
  }
}

TIPS! Do not forget to add your “CustomGroups” to the correct location paths in Web.Config, e.g. the admin part. Or else the users in this group will not be able to access the edit and admin in EPiServer.


  <location path="EPiServer/CMS/admin">
    <system.web>
      <authorization>
        <allow roles="WebAdmins, Administrators, CustomGroup" />
        <deny users="*" />
      </authorization>
    </system.web>
  </location>

After you are finished with compiling, you should be able to create new custom roles in the Admin tool solution I have created earlier.



Then you can click add “Users/ group button” in the “Set Access rights” module. Choose users or group and then add it with the green arrows and click OK.


Then it will appear in the list below. Problem solved!





Happy coding!

6 comments:

  1. Just what we are looking into. I love the EPiServer community sometimes. :)

    ReplyDelete
  2. Thanks for sharing. For reference there are 2 built in providers for the Access Rights UI, one is for the membership system and the other allows you to synchronize any identity which is what is used in federated authentication.You read more about that provider here: http://world.episerver.com/documentation/Items/Developers-Guide/EPiServer-CMS/9/Security/federated-security/

    ReplyDelete
  3. Also love that EPiServer responds to blogs.. :)

    ReplyDelete
  4. Am confused about the solution. Shouldn't you have asp.net identity with a custom member ship provider so that all the existing functionalities provided by episerver be reused easily?

    ReplyDelete
    Replies
    1. When you introduce asp.net identity into your solution you will disable the membership provider. After Episerver added support for OWIN, you can handle security differently in Episerver. That's what these blog posts are about. Read yourself up on OWIN -> https://coding.abel.nu/2014/06/asp-net-identity-and-owin-overview/

      Delete