Authenticating Public Website Users with Sitecore 9.1+ and Facebook

Sep 12, 2019
Byron Calisto

Starting with version 9.0, Sitecore offers the ability to authenticate users using external identity providers based on OAuth and OpenID. This feature is called Federated Authentication, and starting with version 9.1, it is enabled by default. Sitecore 9.1 and later use Federated Authentication with Sitecore Identity server (SI) for CMS admin/editor login. SI is based on IdentityServer4, and you will find many examples on how to customize it with sub-providers to enable Facebook, Google and Azure AD for CMS login. But in many cases, you will want to provide a login for your actual public website, not for the CMS. The following post shows you how you can use an external identity provider for your public website leveraging Federated Authentication. Although this example specifically uses Facebook's identity provider, you can use another provider such as Google or Okta in a similar way (and maybe changing some of the libraries).

What do you need?

This example was done using Sitecore 9.1 Update-1 (9.1.1), so the following NuGet package list (with the libraries you will need for your module's .NET project) are based on what is compatible with Sitecore 9.1.1. For other versions, please check that you use the correct versions of the packages in your Sitecore installation bin directory:

Package Name Version
Microsoft.AspNet.Identity.Core 2.2.1
Microsoft.AspNet.Identity.Owin 2.2.1
Microsoft.AspNet.Mvc 5.2.4
Microsoft.AspNet.Razor 3.2.7
Microsoft.AspNet.WebPages 3.2.7
Microsoft.Extensions.DependencyInjection.Abstractions 2.1.1
Microsoft.Owin 4.0.0
Microsoft.Owin.Security.Facebook 4.0.0
Owin 1.0
Sitecore.Abstractions 9.1.1
Sitecore.Kernel 9.1.1
Sitecore.Mvc 9.1.1
Sitecore.Owin.Authentication 9.1.1

Also, don't forget to create a Facebook Application so you can get an App ID and an App Secret. See Facebook's instructions for how to set up external logins in ASP.NET Core.

How to implement it

The first step is to create the identity provider pipeline that will handle Facebook authentication. This provider must inherit from Sitecore.Owin.Authentication.Pipelines.IdentityProviders.IdentityProvidersProcessor and override the ProcessCore() method:

FacebookIdentityProvider.cs
using Microsoft.Owin;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Security.Facebook;
using Owin;
using Sitecore.Abstractions;
using Sitecore.Diagnostics;
using Sitecore.Owin.Authentication.Configuration;
using Sitecore.Owin.Authentication.Extensions;
using Sitecore.Owin.Authentication.Pipelines.IdentityProviders;
using Sitecore.Owin.Authentication.Services;
using System.Threading.Tasks;
 
namespace Oshyn.Auth.Fb.Pipelines
{
    public class FacebookIdentityProvider : IdentityProvidersProcessor
    {
        protected override string IdentityProviderName => "Facebook";
 
        public FacebookIdentityProvider(
                FederatedAuthenticationConfiguration federatedAuthenticationConfiguration,
                ICookieManager cookieManager,
                BaseSettings settings) :
            base(federatedAuthenticationConfiguration, cookieManager, settings)
        {
        }
 
        protected override void ProcessCore(IdentityProvidersArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
 
            var identityProvider = GetIdentityProvider();
 
            //Apply claims transformations after successful authentication
            var provider = new FacebookAuthenticationProvider
            {
                OnAuthenticated = context =>
                {
                    context.Identity.ApplyClaimsTransformations(
                        new TransformationContext(FederatedAuthenticationConfiguration, identityProvider));
                    return Task.CompletedTask;
                }
            };
 
            //Configure specific options for Facebook Authentication
            var authOptions = new FacebookAuthenticationOptions
            {
                AppId = Sitecore.Configuration.Settings.GetSetting("Oshyn.Auth.Fb.AppId"),
                AppSecret = Sitecore.Configuration.Settings.GetSetting("Oshyn.Auth.Fb.AppSecret"),
                Provider = provider,
                CallbackPath = new PathString("/signin-facebook")
            };
 
            authOptions.Fields.Add("name");
            authOptions.Fields.Add("email");
            authOptions.Scope.Add("email");
 
            //Use the identity provider
            args.App.UseFacebookAuthentication(authOptions);
        }
    }
}

As you can see in the code example, there are two important things to configure. First, you have to make sure the identity claims (more on that later) are transformed to the values defined in configuration (more on that later too) after a user successfully authenticates. The OnAuthentication event allows you to assign a delegate method to perform this (in this example we used a lambda inline block to define this). And second, pass to the identity provider (in this case Facebook) any specific parameters required by it, such as App ID, App Secret, provider, and more. Finally, you need to "connect" the provider to the authentication pipeline.

Before we continue, let me quickly explain the concept of claims. The simplest explanation is that claims are statements made about users, such as name, email, and more. Think of claims as the user information fields. Some of these fields come in different formats, or with different values of what you need in your application. Transformations allow you to take these source claims, and process them to target claims used by your app with different values, names, or formats.

Let's create the configuration patch file for the authentication module. You should deploy this file to your Sitecore installation under App_Config\Include:

Oshyn.Auth.Fb.config
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:security="http://www.sitecore.net/xmlconfig/security/">
  <sitecore role:require="Standalone or ContentDelivery or ContentManagement">
    <!-- These are specific settings for Facebook -->
    <settings>
      <setting name="Oshyn.Auth.Fb.AppId" value="[Your Facebook App ID]" />
      <setting name="Oshyn.Auth.Fb.AppSecret" value="[Your Facebook App Secret]" />
    </settings>
 
    <!-- This configuration points to our custom FacebookIdentityProvider pipeline -->
    <pipelines>
      <owin.identityProviders>
        <processor type="Oshyn.Auth.Fb.Pipelines.FacebookIdentityProvider, Oshyn.Auth.Fb" resolve="true" />
      </owin.identityProviders>
    </pipelines>
 
    <!-- This configuration enables federated authentication for our custom website -->
    <federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">
      <identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
        <mapEntry name="Custom website" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication">
          <sites hint="list">
            <site>website</site>
          </sites>
 
          <identityProviders hint="list:AddIdentityProvider">
            <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='Facebook']" />
          </identityProviders>
 
          <!-- We set the IsPersistentUser to false to avoid storing the user in Sitecore's database -->
          <externalUserBuilder type="Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, Sitecore.Owin.Authentication">
            <IsPersistentUser>false</IsPersistentUser>
          </externalUserBuilder>
           
        </mapEntry>
      </identityProvidersPerSites>
 
      <!-- This configuration defines the identity provider used by our website -->
      <identityProviders hint="list:AddIdentityProvider">
        <identityProvider id="Facebook" 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 Facebook</caption>
          <icon>/Assets/img/icons/facebook.png</icon>
          <domain>extranet</domain>
          <triggerExternalSignOut>false</triggerExternalSignOut>
 
          <!-- We need this transformation to save the full name of the Facebook profile to a custom "long name" claim -->
          <transformations hint="list:AddTransformation">
            <transformation name="Facebook name to long name" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="LongName" />
              </targets>
            </transformation>
          </transformations>
        </identityProvider>
      </identityProviders>
       
      <!-- Map the long name claim to Sitecore's profile FullName property -->
      <propertyInitializer type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication">
        <maps hint="list">
          <map name="Name to FullName" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication">
            <data hint="raw:AddData">
              <source name="LongName" />
              <target name="FullName" />
            </data>
          </map>
        </maps>
      </propertyInitializer>
    </federatedAuthentication>
  </sitecore>
</configuration>

Let's analyze this configuration file section by section:

  • The settings section contains custom settings to store the Facebook App ID and Secret. These are used in the FacebookIdentityProvider pipeline in the FacebookAuthenticationOptions's AppId and AppSecret properties.
  • The pipelines/owin.identityProviders section contains the required configuration that points to our custom FacebookIdentityProvider pipeline.
  • The federatedAuthentication section contains the definition of the identity provider to be used with a specific site in Sitecore. For this example, we assume the published site is defined as <site name="website"...>. You can change the site name to match your site definition. We need to include one more configuration directly to the site definition (more on that later). Also, we need to configure the user builder so it doesn't store the user in Sitecore's core database by setting the IsPersistentUser property to false.
  • The identityProviders/identityProvider section configures the identity provider properties, such as domain to use (for this example we use the extranet domain), and the claims transformations. We are using a claim transformation to extract the Facebook user's full name, and store it in a custom "long name" claim that we will later map to the FullName property of the Sitecore profile. We need to do this because the name claim gets overwritten by the username generated by Sitecore internally.
  • Finally, the propertyInitializer/maps section maps the claims to Sitecore profile properties. Initially for this example, we are only mapping the custom long name claim to the profile's FullName property.

Before you continue, go to your site definition patch file, and make sure to include the loginPage property, following this format: $(loginPage)[site name]/[identity provider name]:

Site.Definition.config
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <sites>
      <site name="website" ... loginPage="$(loginPath)website/Facebook" ... />
    </sites>
  </sitecore>
</configuration>

You need to create a page where you can display the "Log in with Facebook" button for your site. Let's create a component to handle this, that you can include in a page in your Sitecore site. This component will be based on a Controller Rendering, with a View that will display the button:

LoginController.cs
using Microsoft.Extensions.DependencyInjection;
using Sitecore.Abstractions;
using Sitecore.DependencyInjection;
using Sitecore.Pipelines.GetSignInUrlInfo;
using Sitecore.Security.Authentication;
using System.Web.Mvc;
 
namespace Oshyn.Auth.Fb.Controllers
{
    public class LoginController : Controller
    {
        public ActionResult FacebookLogin()
        {
            var corePipelineManager = ServiceLocator.ServiceProvider.GetService<BaseCorePipelineManager>();
            var args = new GetSignInUrlInfoArgs("website", "/");
 
            GetSignInUrlInfoPipeline.Run(corePipelineManager, args);
 
            return View("~/Views/FacebookLogin.cshtml", args.Result);
        }
    }
}
FacebookLogin.cshtml
@model IEnumerable<Sitecore.Data.SignInUrlInfo>
 
@foreach (var signInInfo in Model)
{
    using (Html.BeginForm(null, null, FormMethod.Post, new { @action = signInInfo.Href}))
    {
        <button type="submit">
            <img src="@signInInfo.Icon"/>
            @signInInfo.Caption
        </button>
    }
}

Finally, you want a simple component that shows you the information of the currently signed-in user. Create a View Rendering that displays the current user info:

UserInfo.cshtml
<div id="user-info">
  <p>
    User name: @Sitecore.Context.User.Name<br />
    Full name: @Sitecore.Context.User.Profile.FullName
  </p>
</div>

Deploy your code, go into Sitecore (or Sitecore Rocks), create the renderings, and assign them to pages you can access easily (for this example we assigned the FacebookLogin Controller Rendering to a /login page, and the UserInfo View Rendering to the root page of the website). Publish, and navigate to your login page. Click the "Log in with Facebook" button, and that should redirect you to Facebook's authorization page. After you authorize Facebook to use your account for login, you should see in your page with the UserInfo rendering something like this:

User name: extranet\[some random string]
Full name: [your name as it appears in Facebook]

Enhancements

As you can see, the default external user builder will create a random username instead of using something such as the user's email address. That can be changed with a custom User Builder. This class must inherit from Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, and override the CreateUniqueUserName() method. The following will create a username with the email address of the logged in user:

FacebookUserBuilder.cs
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Extensions.DependencyInjection;
using Sitecore.DependencyInjection;
using Sitecore.Diagnostics;
using Sitecore.Owin.Authentication.Identity;
using Sitecore.Owin.Authentication.Services;
using Sitecore.SecurityModel.Cryptography;
using System;
using System.Diagnostics.CodeAnalysis;
 
namespace Oshyn.Auth.Fb.UserBuilders
{
    public class FacebookUserBuilder : DefaultExternalUserBuilder
    {
        public FacebookUserBuilder(bool isPersistentUser) :
            base(ServiceLocator.ServiceProvider.GetService<ApplicationUserFactory>(),
                 ServiceLocator.ServiceProvider.GetService<IHashEncryption>())
        {
            IsPersistentUser = isPersistentUser;
        }
 
        public FacebookUserBuilder(string isPersistentUser) :
            this(bool.Parse(isPersistentUser)) { }
 
        protected override string CreateUniqueUserName(
            UserManager<ApplicationUser> userManager,
            ExternalLoginInfo externalLoginInfo)
        {
            Assert.ArgumentNotNull(userManager, "userManager");
            Assert.ArgumentNotNull(externalLoginInfo, "externalLoginInfo");
 
            var identityProvider =
                FederatedAuthenticationConfiguration.GetIdentityProvider(externalLoginInfo.ExternalIdentity);
 
            if (identityProvider == null)
            {
                throw new InvalidOperationException("Unable to retrieve identity provider");
            }
 
            return $"{identityProvider.Domain}\\{externalLoginInfo.Email}";
        }
    }
}

Additionally, you will need to replace the federatedAuthentication/identityProvidersPerSites/mapEntry/externalUserBuilder configuration element in your .config patch to point to the custom FacebookUserBuilder class. Also, we are going to include the email mapping for Profile in propertyInitializer/maps:

Oshyn.Auth.Fb.config modifications
<configuration ...>
  <sitecore ...>
 
    <!-- ... other configs ... -->
 
    <federatedAuthentication ...>
      <identityProvidersPerSites ...>
        <mapEntry ...>
          <!-- ... other configs ... -->
          <externalUserBuilder type="Oshyn.Auth.Fb.UserBuilders.FacebookUserBuilder, Oshyn.Auth.Fb">
            <param desc="isPersistentUser">false</param>
          </externalUserBuilder>
        </mapEntry>
      </identityProvidersPerSites>
    </federatedAuthentication>
 
    <!-- ... other configs ... -->
 
    <propertyInitializer ...>
      <maps hint="list">
        <map name="Emailaddress to Email" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
            <data hint="raw:AddData">
              <source name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" />
              <target name="Email" />
            </data>
          </map>
          <map name="Name to FullName" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication">
            <data hint="raw:AddData">
              <source name="LongName" />
              <target name="FullName" />
            </data>
          </map>
      </maps>
    </propertyInitializer>
  </sitecore>
</configuration>

Update your UserInfo View Rendering so it also shows the Email address field:

UserInfo.cshtml updated
<div id="user-info">
  <p>
    User name: @Sitecore.Context.User.Name<br />
    Full name: @Sitecore.Context.User.Profile.FullName<br />
    Email address: @Sitecore.Context.User.Profile.Email
  </p>
</div>

Redeploy, and re-login if necessary. Now you should see in your UserInfo component something like this:

User name: extranet\example@mail.com
Full name: [your name as it appears in Facebook]
Email address: example@mail.com

If you want to implement a Logout, it is as simple as creating a button that calls back to a Controller function that executes Sitecore.Security.Authentication.AuthenticationManager.Logout(), and redirects to your home page.

Conclusion

Sitecore provides all the necessary infrastructure to integrate external identity providers with your application. The example shown in this post was done with Facebook, but it can be easily adapted for Google, Okta or other identity providers.