Active Directory Federation Services (ADFS) for Sitecore login through Identity Service

Introduction

On one of the projects, we had a task to log in users from ADFS and match their groups with Sitecore roles. And of course, we should use IdentityServer for it. Sitecore documentation about the Sitecore Identity server as a federation gateway is available on the following link
https://doc.sitecore.com/developers/93/sitecore-experience-manager/en/use-the-sitecore-identity-server-as-a-federation-gateway.html. Also when you install IdentityServer, you will by default have an example for login AzureAd. For creating this article I also used a blog https://blog.istern.dk/2019/05/15/sitecore-9-1-identityserver-on-premise-ad-via-adf

For connecting ADFS to Sitecore (through Identity Service), we should follow 3 steps:

  1. To create a configuration service that will connect ADFS -> Identity Service -> Sitecore;
  2. To create an XML setting file for setting connection data and mapping fields and roles;
  3. To set up Sitecore

Creating a configuration service

Firstly, we need to create a Class Library project. The project will have 3 CS files:

              ADFSIdentityProvider.cs
namespace Sitecore.IdentityServer.ADFS
{
    public class ADFSIdentityProvider
    {
        public bool Enabled { get; set; }
        public string Authority { get;  set; }
        public string ClientId { get;  set; }
        public string AuthenticationScheme { get; set; }
        public string MetadataAddress { get; set; }
        public string DisplayName { get; set; }
    }
}

The ADFSIdentityProvider class includes setting fields for access to ADFS. These fields are set up from the config file.

             AppSettings.cs
namespace Sitecore.IdentityServer.ADFS
{
    public class AppSettings
    {
        public static readonly string SectionName = "Sitecore:ExternalIdentityProviders:IdentityProviders:ADFS";

        public ADFSIdentityProvider ADFSIdentityProvider { get; set; } = new ADFSIdentityProvider();
    }
}

The AppSettings class seen below is used for retrieving the Setting for the Provider. This class contains two options: SectionName - name section configuration, this configuration we will create on step two; and the ADFSIdentityProvider - an initialized ADFSIdentityProvider, this class was created above.

             ConfigureSitecore.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Sitecore.Framework.Runtime.Configuration;
using System;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Sitecore.IdentityServer.ADFS
{
    public class ConfigureSitecore
    {
        private readonly ILogger<ConfigureSitecore> _logger;
        private readonly AppSettings _appSettings;

        public ConfigureSitecore(ISitecoreConfiguration scConfig, ILogger<ConfigureSitecore> logger)
        {
            this._logger = logger;
            this._appSettings = new AppSettings();
            scConfig.GetSection(AppSettings.SectionName);
            scConfig.GetSection(AppSettings.SectionName).Bind((object)this._appSettings.ADFSIdentityProvider);
        }

        public object IdentityServerConstants { get; private set; }

        public void ConfigureServices(IServiceCollection services)
        {
            ADFSIdentityProvider adfsProvider = this._appSettings.ADFSIdentityProvider;
            if (!adfsProvider.Enabled)
                return;
            this._logger.LogDebug("ADFS: Adding ADFS clientId " + adfsProvider.ClientId + " Authority " + adfsProvider.Authority + "  Scheme " + adfsProvider.AuthenticationScheme);
            new AuthenticationBuilder(services).AddOpenIdConnect(adfsProvider.AuthenticationScheme, adfsProvider.DisplayName, (Action<OpenIdConnectOptions>)(options =>
            {
                options.SignInScheme = "idsrv.external";
                options.SignOutScheme = "idsrv";
                options.RequireHttpsMetadata = false;
                options.SaveTokens = true;
                options.Authority = adfsProvider.Authority;
                options.ClientId = adfsProvider.ClientId;
                options.ResponseType = "id_token";
                options.MetadataAddress = adfsProvider.MetadataAddress;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    NameClaimType = "Display name",
                    RoleClaimType = "roles"
                };
                OpenIdConnectOptions idConnectOptions = options;
                idConnectOptions.Events = new OpenIdConnectEvents()
                {
                    OnAuthenticationFailed = (Func<AuthenticationFailedContext, Task>)(context =>
                    {
                        this._logger.LogWarning("ADFS: [OnAuthenticationFailed] " + context.Exception.Message + ", " + context.Exception.StackTrace);
                        return (Task)Task.FromResult<int>(0);
                    }),
                    OnTokenValidated = (Func<TokenValidatedContext, Task>)(context =>
                    {
                        ClaimsIdentity identity = context.Principal.Identity as ClaimsIdentity;
                        this._logger.LogWarning("ADFS: [OnTokenValidated]");
                        this.LogIdentity(identity);
                        return (Task)Task.FromResult<int>(0);
                    }),
                    OnRemoteFailure = (Func<RemoteFailureContext, Task>)(context =>
                    {
                        this._logger.LogWarning("ADFS: [OnRemoteFailure] " + context.Failure.Message + ", " + context.Failure.StackTrace);
                        return (Task)Task.FromResult<int>(0);
                    }),
                    OnUserInformationReceived = (Func<UserInformationReceivedContext, Task>)(context =>
                    {
                        ClaimsIdentity identity = context.Principal.Identity as ClaimsIdentity;
                        this._logger.LogWarning("ADFS: [OnUserInformationReceived]");
                        this.LogIdentity(identity);
                        return (Task)Task.FromResult<int>(0);
                    })
                };
            }));
        }

        public void LogIdentity(ClaimsIdentity identity)
        {
            this._logger.LogWarning("CustomClaimsTransformation: name: " + identity.Name);
            this._logger.LogWarning("CustomClaimsTransformation: IsAuthenticated " + identity.IsAuthenticated.ToString());
            this._logger.LogWarning("CustomClaimsTransformation: AuthenticationType " + identity.AuthenticationType);
            this._logger.LogWarning("CustomClaimsTransformation: NameClaimType " + identity.NameClaimType);
            this._logger.LogWarning("CustomClaimsTransformation: RoleClaimType: " + identity.RoleClaimType);
            foreach (Claim claim in identity.Claims)
                this._logger.LogWarning("CustomClaimsTransformation: Claim " + claim.Type + " = " + claim.Value);
        }
    }
}

The CongifugreSitecore class handles the communication with the ADFS server. As you can see, we add a lot of log strings, these logs you can see by path identityServer/logs. 

After creating code, you should deploy this on your Identity Server in the sitecoreruntime folder in a production folder. It should be like that:

Creating an XML setting file for setting connection data and mapping fields and roles.

For the final ADFS setup, we need to create a configuration file. This file is presented below.

<?xml version="1.0" encoding="utf-8"?>
<Settings>
  <Sitecore>
    <ExternalIdentityProviders>
      <IdentityProviders>
<ADFS type="Sitecore.Plugin.IdentityProviders.IdentityProvider, Sitecore.Plugin.IdentityProviders">
        <AuthenticationScheme>adfs</AuthenticationScheme>
        <DisplayName>AD</DisplayName>
        <Enabled>true</Enabled>
        <ClientId></ClientId>
        <Authority></Authority>          
        <MetaAddress></MetaAddress>
          <ClaimsTransformations>
            <!--Place transformation rules here. -->
            <ClaimsTransformation1 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
              <SourceClaims>
                <Claim1 type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
              </SourceClaims>
              <NewClaims>
                <Claim1 type="name" />
              </NewClaims>
            </ClaimsTransformation1>
            <ClaimsTransformation2 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
              <SourceClaims>
              <Claim1 type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"/>
              </SourceClaims>
              <NewClaims>
                <Claim1 type="email" />
              </NewClaims>
            </ClaimsTransformation2>
            <ClaimsTransformation3 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="Display name" />
</SourceClaims>
<NewClaims>
<Claim1 type="fullname" />
</NewClaims>
            </ClaimsTransformation3>
            <ClaimsTransformation4 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="http://schemas.xmlsoap.org/claims/Group" value="SC-Authors-QA" />
</SourceClaims>
<NewClaims>
<Claim1 type="http://www.sitecore.net/identity/claims/isAdmin" value="true"/>
</NewClaims>
            </ClaimsTransformation4>
<ClaimsTransformation5 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="http://schemas.xmlsoap.org/claims/Group" value="SC-Authors-Leisure-QA" />
</SourceClaims>
<NewClaims>
<Claim1 type="role" value="sitecore\Author Leisure"/>
</NewClaims>
            </ClaimsTransformation5>
<ClaimsTransformation6 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="http://schemas.xmlsoap.org/claims/Group" value="SC-Managers-Leisure-QA" />
</SourceClaims>
<NewClaims>
<Claim1 type="role" value="sitecore\Leisure Manager"/>
</NewClaims>
            </ClaimsTransformation6>
<ClaimsTransformation7 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="http://schemas.xmlsoap.org/claims/Group" value="SC-Authors-Corp-QA" />
</SourceClaims>
<NewClaims>
<Claim1 type="role" value="sitecore\Corporate Author"/>
</NewClaims>
            </ClaimsTransformation7>
<ClaimsTransformation8 type="Sitecore.Plugin.IdentityProviders.DefaultClaimsTransformation, Sitecore.Plugin.IdentityProviders">
<SourceClaims>
<Claim1 type="http://schemas.xmlsoap.org/claims/Group" value="SC_Global_Manager_QA" />
</SourceClaims>
<NewClaims>
<Claim1 type="role" value="sitecore\Marketing Automation Editors"/>
</NewClaims>
            </ClaimsTransformation8>
          </ClaimsTransformations>
        </ADFS>
      </IdentityProviders>
    </ExternalIdentityProviders>
  </Sitecore>
</Settings>

AuthenticationScheme, ClientId, Authority and MetaAddress properties set up access to the ADFS server.
ClaimsTransformations properties are needed for mapping claims from ADFS with the claims from Sitecore. As you can see, an example config has a mapping name, email, full name and roles.

Note: by default, the identity server has 5 claims that you can map:

If you want to add other claims (from our project’s example, we added full name), you’ll need to add your user claims to
identityserver\sitecore\Sitecore.Plugin.IdentityServer\Config\identityServer.xml

<IdentityResources>
        <SitecoreIdentityResource>
          <Name>sitecore.profile</Name>
          <UserClaims>
            <UserClaim1>name</UserClaim1>
            <UserClaim2>email</UserClaim2>
            <UserClaim3>role</UserClaim3>
            <UserClaim4>http://www.sitecore.net/identity/claims/isAdmin</UserClaim4>            <UserClaim5>http://www.sitecore.net/identity/claims/originalIssuer</UserClaim5>
            <UserClaim6>fullname</UserClaim6>
          </UserClaims>
          <Required>true</Required>
        </SitecoreIdentityResource>
      </IdentityResources>

      <ApiResources>
        <SitecoreApiResource>
          <Name>sitecore.profile.api</Name>
          <DisplayName>Sitecore API</DisplayName>
          <ApiSecrets>
          </ApiSecrets>
          <UserClaims>
            <UserClaim1>name</UserClaim1>
            <UserClaim2>email</UserClaim2>
            <UserClaim3>role</UserClaim3>
            <UserClaim4>http://www.sitecore.net/identity/claims/isAdmin</UserClaim4>                     <UserClaim5>http://www.sitecore.net/identity/claims/originalIssuer</UserClaim5>
     <UserClaim6>fullname</UserClaim6>
          </UserClaims>
        </SitecoreApiResource>
      </ApiResources>

Also you’ll need to create a Sitecore Plugin manifest that should point to your assembly name:

<?xml version="1.0" encoding="utf-8"?>
<SitecorePlugin PluginName="Sitecore.IdentityServer.ADFS" AssemblyName="Sitecore.IdentityServer.ADFS" Version="1.0.0">
  <Dependencies />
  <Tags />
</SitecorePlugin>

Keep the ADFS configuration file and Plugin in identityserver\sitecore\Sitecore.Plugin.IdentityProvider.ADFS\.

Setting up Sitecore

The final step is creating settings in Sitecore. For that, we need to create an xml configuration.

<?xml version="1.0" encoding="utf-8"?>

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:security="http://www.sitecore.net/xmlconfig/security/">
  <sitecore role:require="Standalone or ContentManagement">
    <federatedAuthentication>
      <identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
        <mapEntry name="all sites">
          <identityProviders hint="list:AddIdentityProvider">
            <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='SitecoreIdentityServer/ADFS']" />
          </identityProviders>
        </mapEntry>
      </identityProvidersPerSites>

      <identityProviders hint="list:AddIdentityProvider">
        <identityProvider id="SitecoreIdentityServer/ADFS" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
          <param desc="name">$(id)</param>
          <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
          <caption>Log in with Sitecore Identity: ADFS</caption>
          <icon>/sitecore/shell/themes/standard/Images/24x24/msazure.png</icon>
          <domain>sitecore</domain>
        </identityProvider>
      </identityProviders>

      <propertyInitializer>
        <maps>
          <map name="set email" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
            <data hint="raw:AddData">
              <source name="email" />
              <target name="Email" />
            </data>
          </map>
          <map name="set fullname" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
            <data hint="raw:AddData">
              <source name="fullname" />
              <target name="FullName" />
            </data>
          </map>
        </maps>
      </propertyInitializer>
    </federatedAuthentication>
  </sitecore>
</configuration>

Here you can see standard settings for Sitecore and mapping email and full name. Name, role and isAdmin are mapped by default in Sitecore.