Tuesday, November 14, 2017

Federated Authentication for Sitecore 9 integrating with Azure AD - Step by Step

I started integrating Sitecore 9 with Azure AD and I ended up at two resources (in fact 3, but only 2 public sources, 3rd one was only accessible to people who were registered for Sitecore 9 early access program)

First: Sitecore documentation:

Second:
Bas Lijten blog on enabling the federated authentication with Auth0 helped a lot. and he has also added some sample code in the early access program forum.

But I thought most likely, enterprises would like to integrate with Azure AD for following reasons
  1. Relatively widely used Identity / Federated identity provider
  2. Allows you to sync with your enterprise active directory
  3. And allows you to federate with other organizations given the current era of digital landscape where multiple agencies are involved in your brand story e.g. Technology partners, infrastructure partners, creative agencies and many more.

Note:
  • Sitecore 9 uses ASP.NET Identity and OWIN middleware. Sitecore.Owin and Sitecore.Owin.Authentication are the libraries implemented on top of Microsoft.Owin middleware and supports OpenIDConnect out of the box, with little bit of code you need to add yourself :)
  • The scenario I am covering here is for CM environment. For CD environments it should be pretty straight forward.

So here are the steps to get it working, with some gotchas to keep in mind while troubleshooting

Create an Azure AD service in your azure subscription as shown below


Optional:  Map your AD to your custom domain and then create a TXT record on your domain service provider with the properties as shown below and then verify that on Azure


Register an application on Azure AD through "App Regitration" as shown below. xp0.sc in this case is my sitecore instance name I created locally. Also note the Home page URL


Also configure the ReplyURL where the token will be posted to the same URL as Home page.

Create a few groups representing your sitecore roles e.g. Developer, Administrator, Content Author etc.. (Please take note of the Obeject ID shown when you create these groups as this will be needed in the configuration later for transforming these into Sitecore roles)



Create few users and add different users to different groups

Edit the application manifest file to change the default groupMembershipClaims property from null to SecurityGroup / All. This is needed to send the group claims as part of the token. I struggled to get users log in into Sitecore despite of being authenticated by AD as it doesnt have any group claim and as a result the transformation to convert them into Sitecore roles will not kick-in and Sitecore will prompt saying you do not have appropriate accesses to login. Screenshot of the manifest is shown below and this can be edited by clicking on the newly registered app (xp0.sc in my case) and then clicking on Manifest (as shown below)



At this point your Azure AD instance is setup to authenticate users. Next thing we need to do is integrate it with Sitecore and for that we need to add a patch configuration and a custom processor.

Create a visual studio project by creating a new MVC Empty project and remove all un-necessary files and folders, global.asax, app_data, app_start and web.config file (highlighted below)




Add a nuget source for Sitecore using Tools - > Nuget Package Manager -> Package Manager Settings as shown below


Add web.config from your Sitecore instance into the visual studio solution

Create a publish profile (file system) to publish your project output into Sitecore root folder

Exclude files to be deployed as part of the publish in the custom publish profile you created , as indicated below
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <LastUsedBuildConfiguration>Debug</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish />
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <publishUrl>C:\inetpub\wwwroot\xp0.sc</publishUrl>
    <DeleteExistingFiles>False</DeleteExistingFiles>
    <ExcludeFilesFromDeployment>bin\Sitecore.Kernel.dll;bin\Sitecore.Mvc.dll;bin\Sitecore.Mvc.Presentation.dll;bin\Sitecore.Mvc.Analytics.dll;web.config</ExcludeFilesFromDeployment>
  </PropertyGroup>
</Project>
Once your AD instance is setup, and your solution is created, you can start integrating it with Sitecore and the first step would be to enable Sitecore.Owin.Authentication.Enabler.config file which is available in App_Config\Include\Examples.

Please note, all your custom config files must go in Include folder and you should not be changing anything in Sitecore layer (layered configuration). Below is the snapshot of my solution view where configuration files are created

Also add a custom patch configuration file to include your other federated authentication configurations. I will explain each bit of it in inline comments in the configuration below
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
  <sitecore role:require="Standalone or ContentDelivery or ContentManagement">
    <settings>
      <!-- Below settings describes Azure AD details to which we are integrating with -->
      <setting name="ClientId" value="application ID when you register your application into Azure through APP registration" />
      <setting name="AADInstance" value="https://login.microsoftonline.com/{0}" />
      <setting name="Tenant" value="your azure active directory e.g. badalkotecha.onmicrosoft.com" />
      <setting name="PostLogoutRedirectURI" value="https://xp0.sc/sitecore/login" />
      <setting name="RedirectURI" value="https://xp0.sc/sitecore" />
    </settings>
    <pipelines>
      <owin.identityProviders>
        <!-- This is the custom processor that gets executed when azure AD posts the token to Sitecore -->
        <processor type="SitecoreFedAuthWithAzureAD.Pipelines.CustomAzureADIdentityProvider, SitecoreFedAuthWithAzureAD" resolve="true" />
      </owin.identityProviders>
    </pipelines>
    <federatedAuthentication>
      <identityProviders hin="list:AddIdentityProvider">
        <identityProvider id="xp0.sc.azureAD" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
          <param desc="name">$(id)</param>
          <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
          <caption>Sign-in with Azure Active Directory</caption>
          <domain>sitecore</domain>
          <icon>/sitecore/shell/themes/standard/Images/24x24/msazure.png</icon>
          <transformations hint="list:AddTransformation">
            <!-- you need to have and Idp Claim for this to work -->
            <transformation name="Idp Claim" ref="federatedAuthentication/sharedTransformations/setIdpClaim" />
            <!-- This is to transform Azure group into Sitecore Role. The claim value below is the object id that needs to be copied from Azure -->
            <transformation name="Transform to Sitecore DEV Role" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="groups" value="2656b61c-748a-4d52-904c-099044c4dcd5" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="Sitecore\Developer" />
              </targets>
              <keepSource>true</keepSource>
            </transformation>
          </transformations>
        </identityProvider>
      </identityProviders>
      <!-- Property initializer assigns claim values to sitecore user properties -->
      <propertyInitializer type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication">
        <maps hint="list">
          <map name="email claim" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication">
            <data hint="raw:AddData">
              <!--claim name-->
              <source name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" />
              <!--property name-->
              <target name="Email" />
            </data>
          </map>
          <map name="Name claim" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication">
            <data hint="raw:AddData">
              <!--claim name-->
              <source name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" />
              <!--property name-->
              <target name="Name" />
            </data>
          </map>
        </maps>
      </propertyInitializer>
      <identityProvidersPerSites>
        <mapEntry name="all" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication">
          <sites hint="list">
            <site>shell</site>
            <site>login</site>
            <site>admin</site>
            <site>service</site>
            <site>modules_shell</site>
            <site>modules_website</site>
            <site>website</site>
            <site>scheduler</site>
            <site>system</site>
            <site>publisher</site>
          </sites>
          <!-- Registered identity providers for above providers -->
          <identityProviders hint="list:AddIdentityProvider">
            <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='xp0.sc.azureAD']" />
          </identityProviders>
          <!-- ExternalUserBuilder is what creates a user with customusername in Sitecore and assigns roles based on claim transformation configured above -->
          <externalUserBuilder type="Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, Sitecore.Owin.Authentication">
            <param desc="isPersistentUser">true</param>
          </externalUserBuilder>
        </mapEntry>
      </identityProvidersPerSites>
    </federatedAuthentication>
  </sitecore>
</configuration>
Add below nuget package references for us to use OpenIDConnect as the identity provider
Microsoft.Owin.Security.OpenIdConnect
Sitecore.Owin
Sitecore.Owin.Authentication

Add code to process your authentication events

using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using Sitecore;
using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Owin.Authentication.Configuration;
using Sitecore.Owin.Authentication.Pipelines.IdentityProviders;
using Sitecore.Owin.Authentication.Services;
using System.Globalization;
using System.Threading.Tasks;

namespace SitecoreFedAuthWithAzureAD.Pipelines
{
    public class CustomAzureADIdentityProvider : IdentityProvidersProcessor
    {

        public CustomAzureADIdentityProvider(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration) : base(federatedAuthenticationConfiguration)
        {
        }

        protected override string IdentityProviderName => "xp0.sc.azureAD";

        protected override void ProcessCore([NotNull] IdentityProvidersArgs args)
        {
            Assert.ArgumentNotNull(args, nameof(args));

            var identityProvider = this.GetIdentityProvider();
            var authenticationType = this.GetAuthenticationType();

            string aadInstance = Settings.GetSetting("AADInstance");
            string tenant = Settings.GetSetting("Tenant");
            string clientId = Settings.GetSetting("ClientId");
            string postLogoutRedirectURI = Settings.GetSetting("PostLogoutRedirectURI");
            string redirectURI = Settings.GetSetting("RedirectURI");

            string authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

            args.App.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                Caption = identityProvider.Caption,
                AuthenticationType = authenticationType,
                AuthenticationMode = AuthenticationMode.Passive,
                ClientId = clientId,
                Authority = authority,
                PostLogoutRedirectUri = postLogoutRedirectURI,
                RedirectUri = redirectURI,

                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    SecurityTokenValidated = notification =>
                    {
                        var identity = notification.AuthenticationTicket.Identity;

                        foreach (var claimTransformationService in identityProvider.Transformations)
                        {
                            claimTransformationService.Transform(identity, new TransformationContext
                            {
                                IdentityProvider = identityProvider
                            });
                        }

                        notification.AuthenticationTicket = new AuthenticationTicket(identity, notification.AuthenticationTicket.Properties);

                        return Task.CompletedTask;
                    }

                }
            });
        }
    }
}


Few Gotchas:
  • Make sure you are not logged in into azure portal as that also uses the azure ad single sign on and the moment you click on federated sign in button in Sitecore, it will take your current session cookie with azure ad and return claims for that user without even asking you to enter credentials. I therefore used incognito window always during debugging.
  • Also note that it does not use the credentials entered in the default sitecore login screen.
  • When you are experimenting with Azure AD always use incognito mode (as mentioned above)
  • Sitecore logout does not kill session with azure ad at this point based on my experiment, but you can extend this pipeline. I have raised  ticket with Sitecore on this.
  • Also ensure that you edit application manifest for your registered application with AD to send groupClaims as indicated in my blog. In the absense of that and the transformation rule for group claim to Sitecore role, you will not be able to login to Sitecore