allBlogsList

Sitecore FederatedAuth Transformation Mappings

Manage Group-to-Role Mappings in Sitecore

Recently, I wrote about how to configure federated authentication via Okta and Sitecore IdentityServer. Per the post, hooking up federated authentication is fairly simple. Using the basic methodology for configuring your authentication, you can map your groups to Sitecore roles via configuration. I am not a fan of this approach for two reasons:

  1. It is a change we have to make to IdentityServer
  2. It is a change we have to maintain and deploy, potentially requiring an outage of your app.

I already have a post on how to add support for doing your group-to-role mappings in Sitecore. However, that version was done against an implementation without Sitecore IdentityServer. Additionally, after looking back on that post, I discovered a better way!! 

TL;DR - this one is a lot of text, and not a lot of pictures. There is a GitHub repo: Sitecore-GroupToRoleMapper.

To start, we are going to have to make a small change to IdentityServer. When I was trying to re-implement the group-to-role mappings, I ran into a problem where I didn't have any groups on the Sitecore side. This makes sense because IdentityServer is acting as a proxy to our Identity Provider (IDP). While the IDP is sending groups back to IdentityServer, those groups are not being sent back to Sitecore; this is because Sitecore is not asking for the groups. To get Sitecore to ask for groups is a simple configuration change:

<Settings>
  <Sitecore>
    <IdentityServer>
      <IdentityResources>
        <SitecoreIdentityResource>
          <UserClaims>
            <UserClaim6>groups</UserClaim6>
          </UserClaims>
        </SitecoreIdentityResource>
      </IdentityResources>
    </IdentityServer>
  </Sitecore>
</Settings>

Patch files work very similarly in Sitecore IdentityServer as they do in Sitecore. We save the above file as an XML file and deploy it to IdentityServer, and it will add the groups claim to the list of scopes returned from IdentityServer to Sitecore on successful authentication of the user. That is all we have to do on the IdentityServer side, so now we pop over to Sitecore.

The first thing to do on the Sitecore side of things is to create the custom dropdown field that will fill with the list of available roles in Sitecore. I am not going to put details on how to do that in this post; it is the same implementation as what is done here.

The really cool part is an improved mechanism for mapping things. The identity claims coming from IdentityServer are mapped to Sitecore "things" via transformations the same as IdentityServer. There are a couple of OOTB transformations that are available, including the transformation for mapping the role claim to http://schemas.microsoft.com/ws/2008/06/identity/claims/role:

<transformation name="role to long role" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
  <sources hint="raw:AddSource">
    <claim name="role"/>
  </sources>
  <targets hint="raw:AddTarget">
    <claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role"/>
  </targets>
  <keepSource>false</keepSource>
</transformation>
    

This means we just have to transform our groups to a role claim. Last time, I interjected into an existing transformation. This time, we are going to create our own transformation mapper. Create the class:

using System.Linq;
using System.Security.Claims;
using Foundation.GroupToRoleMapper.Extensions;
using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Owin.Authentication.Services;

namespace Sitecore.GroupToRoleMapper.IdentityServer.Transformations
{
  public class ClaimsToRolesTransformation : Transformation
  {
    public override void Transform(ClaimsIdentity identity, TransformationContext context)
    {
      var groups = identity.FindAll("groups");

      var masterDatabase = Factory.GetDatabase("master");
      var rootTransformationItem = masterDatabase.GetItem(Settings.GetSetting(Constants.Settings.RoleTransformationsRootItemId, "{D65C7354-4B9F-409B-A621-2B023D435E58}"));

      if (rootTransformationItem == null)
      {
        return;
      }

      var transformations = rootTransformationItem.Children.Where(rti => rti.IsDerived(Templates.GroupToRoleTransformations.Id)).ToList();

      Log.Info($"Processing groups-to-role for user: {string.Join(" | ", groups.Select(g => g.Value))}", this);

      foreach (var transformation in transformations)
      {
        var source = transformation.Fields[Templates.GroupToRoleTransformations.Fields.GroupName]?.Value.ToLowerInvariant();
        var targetRole = transformation.Fields[Templates.GroupToRoleTransformations.Fields.SitecoreRole]?.Value;

        if (groups.Any(g => g.Value.Equals(source, System.StringComparison.InvariantCultureIgnoreCase)))
        {
          var claim = targetRole.Equals(Constants.AdminText) 
            ? new Claim(Constants.AdminClaim, bool.TrueString.ToLower()) 
            : new Claim("role", targetRole);

          identity.AddClaim(claim);
        }
      }
    }
  }
}        
    

Wall of code! Let's touch on the important bits:

    • You need Sitecore.Owin.Authentication and System.Security.Claims
    • You will need group-to-role mappings defined in Sitecore. I like to put them in a Settings root folder, and then add the ID of that item in a config file.
    • Take special note of the order of operations. Note that this gets all transformations, but uses groups as the statement of record for which transformations get applied to the user.
    • I added special logic to give people Admin access to the system. It is a different claim than a standard role.

Now we just have to patch this guy in. Based on some venting in the Community Slack, I will reiterate an important note: ALWAYS USE PATCH FILES!!

<configuration>
  <sitecore xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <federatedAuthentication>
      <identityProviders>
    	<identityProvider id="SitecoreIdentityServer" >
    	  <transformations hint="list:AddTransformation">
    		<transformation name="map roles from Sitecore" 
    						type="Foundation.GroupToRoleMapper.IdentityServer.Transformations.ClaimsToRolesTransformation, Foundation.GroupToRoleMapper"
    						patch:after="transformation[@type='Sitecore.Owin.Authentication.IdentityServer.Transformations.ApplyAdditionalClaims, Sitecore.Owin.Authentication.IdentityServer']" resolve="true" />
    	  </transformations>
    	</identityProvider>
      </identityProviders>
    </federatedAuthentication>
  </sitecore>
</configuration>
    

This patch file will add our transformer to the list of transformations that are processed on the IdentityClaims object returned from IdentityServer. In this case, I am patching it in right after one of Sitecore's OOTB transformation. The order is only important in that it has to be applied BEFORE the role to long role transformation.

We deploy the code and config file out to Sitecore, and that is it. You can add your mappings in Sitecore, authenticate with your external IDP, and do all of the things your permissions would grant you. This implementation method should work for federated authentication with and without Sitecore IdentityServer, as well as for any external IDP provided they can send groups as a claim on the authentication response.

This is pretty handy to avoid having to push configurations out to the app all the time. It also moves some of the security management for Sitecore off of a developer, and it is always nice to have less to do.

A full implementation of the mapping logic can be found on GitHub here: Sitecore-GroupToRoleMapper. Enjoy!

Not in that repo is the patch file for IdentityServer. A version of that can be found in my Okta repository.