Monday, November 18, 2019

Uploading SVGs to the Media Library

There are a number of blog posts that cover the upload of SVG files into the Media Library. What I ran into was a little different, so I figured I would blog about it for future reference.

The issue was pertaining to the height and width of the uploaded asset and it not being the same as the actual size of the original media.

Below you'll find the code snippet that solves this issue for the situation I was dealing with. The code uses Svg Nuget package to read the structure of SVG XML and extract the width and height parameters either from the document size properties or from the properties of the Bounds one.


using System;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Resources.Media;
using Sitecore.SecurityModel;
using Svg;

namespace JCore.Foundation.SitecoreExtensions.Resources.Media
{
    public class SvgMedia : ImageMedia
    {
        /// clone.
        public override Sitecore.Resources.Media.Media Clone()
        {
            Assert.IsTrue(this.GetType() == typeof(SvgMedia), "The Clone() method must be overridden to support prototyping.");
            return new SvgMedia();
        }

        /// The update meta data. 
        /// The media stream.
        protected override void UpdateImageMetaData(MediaStream mediaStream)
        {
            var mediaItem = mediaStream?.MediaItem;
            if (mediaItem != null)
            {
                try
                {
                    SvgDocument document = SvgDocument.Open(mediaItem.GetMediaStream());
                    if (document != null)
                    {
                        using (new SecurityDisabler())
                        {
                            using (new EditContext(mediaItem))
                            {
                                mediaItem.InnerItem["Width"] = !document.Width.ToString().Contains("%") ? ((int)document.Width.Value).ToString() : ((int)document.Bounds.Width).ToString() ;
                                mediaItem.InnerItem["Height"] = !document.Height.ToString().Contains("%") ? ((int)document.Height.Value).ToString() : ((int)document.Bounds.Height).ToString();
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Log.Error(ex.Message,ex, this);
                }
            }
        }
    }
}

And a config file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
      <medialibrary>
          <mediatypes>
              <mediatype extensions="svg" name="SVG image">
                  <prototypes>
                      <media type="Sitecore.Resources.Media.SvgMedia, Sitecore.Kernel">
                          <patch:delete>
                      </patch:delete></media>
                      <media type="JCore.Foundation.SitecoreExtensions.Resources.Media.SvgMedia, JCore.Foundation.SitecoreExtensions">
                  </media></prototypes>
              </mediatype>
          </mediatypes>
      </medialibrary>
  </sitecore>
</configuration>

Wednesday, September 18, 2019

Copying items from one Sitecore database into another using SQL.

On one of the recent projects I had to a lot of SQL data manipulations in Sitecore databases, so I figured I would share a script I used. This particular script is for migrating of media items from Sitecore to Sitecore.

declare @ancestorId varchar(56)
set @ancestorId = '[puth the root ID of the items here]'

INSERT INTO [atcc_Master].[dbo].[Items]
SELECT distinct i.ID, i.Name, i.TemplateID, i.MasterID, i.ParentID, i.Created, i.Updated
  FROM [ATCCWeb].[dbo].[Items] i
    join [ATCCWeb].[dbo].[Descendants] d on d.Descendant = i.ID
left join [atcc_Master].[dbo].[Items] i2 on i.ID = i2.ID
where i2.ID is null and d.Ancestor = @ancestorId
GO

INSERT INTO [atcc_Master].[dbo].[VersionedFields]
SELECT distinct vf.[Id]
      ,vf.[ItemId]
      ,vf.[Language]
      ,1
      ,vf.[FieldId]
      ,vf.[Value]
      ,vf.[Created]
      ,vf.[Updated]
  FROM [ATCCWeb].[dbo].[VersionedFields] vf
  join [ATCCWeb].[dbo].[Descendants] d on d.Descendant = vf.ItemId
  left join [atcc_Master].[dbo].[VersionedFields] vf2 on vf2.ItemId = vf.ItemId and vf2.FieldId = vf.FieldId
where vf2.ID is null and
d.Ancestor = @ancestorId

GO

INSERT INTO [atcc_Master].[dbo].[UnversionedFields]
SELECT distinct vf.[Id]
      ,vf.[ItemId]
      ,vf.[Language]
      ,vf.[FieldId]
      ,vf.[Value]
      ,vf.[Created]
      ,vf.[Updated]
  FROM [ATCCWeb].[dbo].[UnversionedFields] vf
  join [ATCCWeb].[dbo].[Descendants] d on d.Descendant = vf.ItemId
  left join [atcc_Master].[dbo].[UnversionedFields] vf2 on vf2.ItemId = vf.ItemId and vf2.FieldId = vf.FieldId
where vf2.ID is null and
d.Ancestor = @ancestorId

GO

Sitecore driven Augmented Reality


Augmented Reality (AR) and Virtual Reality (VR) are getting more and more traction. Developers and marketers together are coming up with endless ways to bring what was once upon a time-limited to the computer world into our everyday reality.

IKEA Place application allows users to try furniture in their own house/apartment/room. It also has a very cool object search feature that helps shoppers find a piece of furniture that they might see in someone else’s house on the ikea.com.

Retailers across the work are exploring AR providing various “try before you buy” applications, within the store and outside of it.

Remember Snapchat or PokemonGo? Possibilities are endless…

Now, Sitecore developers and architects, marketers and content authors, imagine if you had an AR application and would be able to control from Sitecore what shows up in it and where. You would also be able to personalize AR experience.

The idea behind this experiment is simple. I wanted to figure out if it is possible to make Augmented Reality experience Sitecore driven. In the end, I was able to store and retrieve trained markers and models as well as define what should happen when a marker is recognized.

On the screenshot below you see the result of this experiment. My marker image was recognized, and a video loaded.




If you would like to try the below example yourself, you’ll need:

  • Sitecore 9.1.1 or any other version of Sitecore
  • Unity version 2018.3.12f1
  • Wikitude (AR SDK)
  • Visual Studio 2017
For Wikitude you'll need a license. Luckily they provide an educational license for free. You just need to create an account on their web site and request the license. I received mine when I took the Udemy "A Beginner's Guide to Augmented Reality with Unity" course by Penny de Byl.

Markers

Wikitude Studio online was the tool that I use to create markers. 2D markers have .wtc extension that Sitecore doesn't have any issues with storing and serving.

In Wikitude Studio I uploaded two of my target images, both of them received three stars, which means that they have enough contrast and are suitable to be used as markers.



The icon with a little "w" on it allows you to request the trained marker to be sent to you over the email.

Sitecore

In Sitecore, I created the following structure for a two-target marker that I created. 


The "Business" item represents a marker with two targets. The "Marker" field points to the marker file in the Media Library.

The child items have the following fields:
  • Target Name - for determining which target in the marker was recognized
  • Video - a video to load in the place of the marker target in AR
  • Click Url - when the video has finished playing, it becomes clickable.
  • Model (for 3D markers that is work-in-progress)



To make this data available to Unity and the mobile application, I created a web service that produces the following output:


Now to Unity...

Unity

In Unity, I created a new project and imported Wikitude SDK. 


I replaced the Default Camera with a Wikitude one. Directional Light stays as is. I also added an Image Tracker from Wikitude SDK and a Trackable pannel that the videos will be loaded into. The Sitecore GameObject is simply for loading the initial script to make a call to the Sitecore service for the data.


Lets look at the configurations for these GameObjects a little closer.

ImageTracker

In the ImageTracker Target Collection field you'll see the value of Wikitude/twotargets.wtc. It has to be set even if the actual target collection file comes from Sitecore.


On the bottom of the ImageTracker properties window, notice "Load from Sitecore" script. 
This script was created in the Unity Assets folder.


Double-click on the script asset file to load it in Visual Studio (assuming you already have it installed). Here is the code for this script asset.

using System.Collections;
using System.IO;
using System.Linq;
using System.Net;
using UnityEngine;
using UnityEngine.Video;
using Wikitude;

public class LoadFromSitecore : MonoBehaviour
{
    private MarkerModelReference Marker;
    private GameObject Trackable;

    // Start is called before the first frame update
    void Start()
    {
        var serviceUrl = "http://ar.sitecore/api/sitecore/services/markers";
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(serviceUrl);
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        StreamReader reader = new StreamReader(response.GetResponseStream());
        string jsonResponse = reader.ReadToEnd();
        Marker = JsonUtility.FromJson(jsonResponse);
        Marker.Targets = JsonHelper.GetObjectArray(jsonResponse, "Targets");
        Debug.Log(Marker.Targets.Count());

        GameObject trackerObjectNew = new GameObject("ImageTracker");
        ImageTracker imageTrackerNew = trackerObjectNew.AddComponent();

        Trackable = new GameObject("ImageTrackable");
        ImageTrackable imageTrackable = Trackable.AddComponent();
        imageTrackable.transform.SetParent(imageTrackerNew.transform, false);
        imageTrackable.OnImageRecognized.AddListener(OnImageRecognized);
        imageTrackable.OnImageLost.AddListener(OnImageLost);

        imageTrackerNew.TargetSourceType = TargetSourceType.TargetCollectionResource;
        imageTrackerNew.TargetCollectionResource = new TargetCollectionResource();
        imageTrackerNew.TargetCollectionResource.UseCustomURL = true;
        imageTrackerNew.TargetCollectionResource.TargetPath = Marker.MarkerUrl;

        Debug.Log(Marker.Targets.Count());
        imageTrackerNew.MaximumNumberOfConcurrentTrackableTargets = Marker.Targets.Count() > 5 ? 5 : Marker.Targets.Count();
        Debug.Log(imageTrackerNew.TargetCollectionResource.TargetPath);

    }

    public void OnImageLost(ImageTarget recognizedTarget)
    {
        Debug.Log("OnImageLost");
        Debug.Log(recognizedTarget.Drawable.gameObject);
        Destroy(recognizedTarget.Drawable.gameObject);
    }

    public void OnImageRecognized(ImageTarget recognizedTarget)
    {
        Debug.Log("OnImageRecognized");
        if (Marker == null || Marker.Targets == null)
        {
            return;
        }

        Debug.Log(recognizedTarget.Name);

        var videoPlane = GameObject.CreatePrimitive(PrimitiveType.Plane);
        videoPlane.transform.SetParent(Trackable.transform, false);

        // Set the newAugmentation to be a child of the Drawable.
        videoPlane.transform.parent = recognizedTarget.Drawable.transform;

        // Position the augmentation relative to the Drawable by using the localPosition.
        videoPlane.transform.localPosition = Vector3.zero;
        videoPlane.transform.localScale = new Vector3(-1f, -1f, -1f);
        videoPlane.name = recognizedTarget.Name;

        var target = Marker.Targets.FirstOrDefault(t => t.TargetName == recognizedTarget.Name);
        if (target == null || string.IsNullOrWhiteSpace(target.VideoUrl))
        {
            Debug.Log("target.ModelUrl is empty");
            return;
        }

        StartCoroutine(playVideo(target.VideoUrl, videoPlane));
    }

    IEnumerator playVideo(string url, GameObject videoPlane)
    {
        Debug.Log("playVideo: " + url);
        //Add VideoPlayer to the GameObject
        var videoPlayer = gameObject.AddComponent();

        //Disable Play on Awake for both Video and Audio
        videoPlayer.playOnAwake = false;

        videoPlayer.source = VideoSource.Url;
        videoPlayer.loopPointReached += EndReached;
        videoPlayer.url = url;
        videoPlayer.name = videoPlane.name;
        videoPlayer.Prepare();
        //videoPlayer.transform.SetParent(videoPlane.transform);

        //Wait until video is prepared
        while (!videoPlayer.isPrepared)
        {
            Debug.Log("Preparing Video");
            yield return null;
        }

        Debug.Log("Done Preparing Video");

        //Assign the Texture from Video to RawImage to be displayed
        var renderer = videoPlane.GetComponent();
        renderer.material.mainTexture = videoPlayer.texture;

        //// Get the video width and height
        var videoWidth = videoPlayer.width;
        var videoHeight = videoPlayer.height;

        if (videoWidth > 0 && videoHeight > 0)
        {
            // Scale the video plane to match the video aspect ratio
            float aspect = videoHeight / (float)videoWidth;

            // Flip the plane as the video texture is mirrored on the horizontal
            videoPlane.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f * aspect);
        }

        //Play Video
        videoPlayer.Play();
    }

    void EndReached(VideoPlayer videoPlayer)
    {
        videoPlayer.Stop();
        videoPlayer.SendMessageUpwards("VideoEnded", videoPlayer.name, SendMessageOptions.DontRequireReceiver);

        Debug.Log("video ended");
    }

    void VideoEnded(string videoName)
    {
        Debug.Log("VideoEnded");
    }
}


To be able to use a panel as Drawable in my ImageTracker/Trackable, the panel had to be converted into Prefab.


This Prefab is referenced in Trackable.


The next item to inspect is the "Sitecore" GameObject. In the properties window I have the following:


Notice that I am using the same script file. 

At this point, everything is ready to run a test. Clicking the play button at the top of the Unity window, pointing the camera to my printed on paper target images, and here you go - the Sitecore-driven AR experience. 


Friday, May 10, 2019

Federated Authentication with Insite Identity Server - Part 2

Customizing Insite Identity Server 

On the Insite side, we had to change the Identity Server ApplicationOptions to honor parameters that are being passed in from Sitecore:

EnableSignOutPrompt = false
EnablePostSignOutAutoRedirect = true 

These parameters the way they are set above allow for the log out redirect to bring the user back to Sitecore after the log out on the Insite side.

To make this happen we created the following class:

[BootStrapperOrder(26)]
    public class ConfigureIdentityServer : IStartupTask, IMultiInstanceDependency, IDependency, IExtension
    {
        public ConfigureIdentityServer()
        {
            DbMigrations.Run("identity");
            LogProvider.SetCurrentLogProvider(new NoopLogProvider());
            CookieAuthenticationOptions authenticationOptions = new CookieAuthenticationOptions();
            authenticationOptions.AuthenticationType = "ApplicationCookie";
            authenticationOptions.LoginPath = new PathString("/RedirectTo/SignInPage");
            authenticationOptions.ExpireTimeSpan = TimeSpan.FromMinutes(20.0);
            authenticationOptions.Provider = new CookieAuthenticationProvider()
            {
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<IdentityUserManager, IdentityUser>(TimeSpan.FromMinutes(20.0), (manager, user) => manager.CreateIdentityAsync(user, "ApplicationCookie"))
            };
            SecurityOptions.CookieOptions = authenticationOptions;
            ConfigureIdentityServer.ConfigureIdentityServerOptions();
            AntiForgeryConfig.UniqueClaimTypeIdentifier = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name";
        }

        /// <summary>
        /// Configures the identity server options.
        /// </summary>
        private static void ConfigureIdentityServerOptions()
        {
            string str = AppSettingProvider.Current["IdentityServerUrl"];
            SecurityOptions.IssuerUri = str.IsBlank() ? "http://www.dummy.com" : str;
            SecurityOptions.RequireSsl = AppSettingProvider.Current["IdentityServerRequireSsl"].EqualsIgnoreCase("true");
        }

        public void Run(IAppBuilder app, HttpConfiguration config)
        {
            app.CreatePerOwinContext(new Func<Insite.IdentityServer.AspNetIdentity.IdentityDbContext>(Insite.IdentityServer.AspNetIdentity.IdentityDbContext.Create));
            app.CreatePerOwinContext(new Func<IdentityFactoryOptions<IdentityUserManager>, IOwinContext, IdentityUserManager>(IdentityUserManager.Create));
            app.CreatePerOwinContext(new Func<IdentityFactoryOptions<IdentitySignInManager>, IOwinContext, IdentitySignInManager>(IdentitySignInManager.Create));
            app.UseKentorOwinCookieSaver();
            IdentityServerBearerTokenAuthenticationOptions adminTokenOptions = new IdentityServerBearerTokenAuthenticationOptions()
            {
                Authority = SecurityOptions.IssuerUri,
                RequiredScopes = new string[1]
              {
          "isc_admin_api"
              },
                NameClaimType = "preferred_username",
                RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
                TokenProvider = new MixedOAuthBearerAuthenticationProvider(),
                IssuerName = SecurityOptions.IssuerUri,
                SigningCertificate = Certificate.Get(),
                ValidationMode = ValidationMode.Local
            };
            MapExtensions.Map(app, "/admin", admin =>
            {
                admin.UseIdentityServerBearerTokenAuthentication(adminTokenOptions);
                admin.MapSignalR();
            });

            MapExtensions.Map(app, "/secureElmah", admin => admin.UseIdentityServerBearerTokenAuthentication(adminTokenOptions));
            MapExtensions.Map(app, "/userfiles/_system", admin => admin.UseIdentityServerBearerTokenAuthentication(adminTokenOptions));
            MapExtensions.Map(app, "/api/v1/admin", admin => admin.UseIdentityServerBearerTokenAuthentication(adminTokenOptions));
            MapExtensions.Map(app, "/contentadmin", admin => admin.UseIdentityServerBearerTokenAuthentication(adminTokenOptions));
            MapExtensions.Map(app, "/webpageconverter", admin => admin.UseIdentityServerBearerTokenAuthentication(adminTokenOptions));
            foreach (string micrositeName in GetMicrositeNames())
            {
                MapExtensions.Map(app, "/" + micrositeName + "/contentadmin", admin => admin.UseIdentityServerBearerTokenAuthentication(adminTokenOptions));
            }

            foreach (string authenticationPath in SiteStartup.Instance.GetAdditionalAdminAuthenticationPaths())
            {
                MapExtensions.Map(app, authenticationPath, admin => admin.UseIdentityServerBearerTokenAuthentication(adminTokenOptions));
            }

            app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions()
            {
                Authority = SecurityOptions.IssuerUri,
                RequiredScopes = new string[1]
              {
          "iscapi"
              },
                NameClaimType = "preferred_username",
                RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
                TokenProvider = new MixedOAuthBearerAuthenticationProvider(),
                IssuerName = SecurityOptions.IssuerUri,
                SigningCertificate = Certificate.Get(),
                ValidationMode = ValidationMode.Local
            });
            app.UseCookieAuthentication(SecurityOptions.CookieOptions);
            app.UseExternalSignInCookie("ExternalCookie");
            MapExtensions.Map(app, "/identity", identityApp => identityApp.UseIdentityServer(new IdentityServerOptions()
            {
                SiteName = "ATCC Identity Server",
                CspOptions = new CspOptions() { Enabled = false },
                SigningCertificate = Certificate.Get(),
                Factory = this.ConfigureCustomFactory(Factory.Configure(ConnectionStringProvider.Current.ConnectionStringName)),
                AuthenticationOptions = new AuthenticationOptions()
                {
                    IdentityProviders = new Action<IAppBuilder, string>(ConfigureIdentityProviders),
                    EnableSignOutPrompt = false,
                    EnablePostSignOutAutoRedirect = true
                },
                IssuerUri = SecurityOptions.IssuerUri,
                RequireSsl = SecurityOptions.RequireSsl
            }));
            IdentityUserManager.DataProtectionProvider = app.GetDataProtectionProvider();
        }

        private static IList<string> GetMicrositeNames()
        {
            return DependencyLocator.Current.GetInstance<IUnitOfWorkFactory>().GetUnitOfWork().GetRepository<Website>().GetTable().Select(o => o.MicroSiteIdentifiers).AsEnumerable().SelectMany(o => o.Split(',', ';')).Select(o => o.Trim()).Where(o => !o.IsBlank()).ToList();
        }

        private static void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
        {
            ConfigureIdentityServer.ConfigureFacebookLogin(app, signInAsType);
            ConfigureIdentityServer.ConfigureGoogleLogin(app, signInAsType);
            ConfigureIdentityServer.ConfigureWindowsAuthentication(app, signInAsType);
        }

        private static void ConfigureGoogleLogin(IAppBuilder app, string signInAsType)
        {
            GoogleSsoSettings googleSsoSettings = SettingsGroupProvider.Current.Get<GoogleSsoSettings>(default(Guid?));
            if (!googleSsoSettings.Enabled)
            {
                return;
            }

            string clientId = googleSsoSettings.ClientId;
            string clientSecret = googleSsoSettings.ClientSecret;
            if (clientId.IsBlank() || clientSecret.IsBlank())
            {
                return;
            }

            IAppBuilder app1 = app;
            GoogleOAuth2AuthenticationOptions options = new GoogleOAuth2AuthenticationOptions
            {
                Caption = "Google",
                ClientId = clientId,
                ClientSecret = clientSecret,
                SignInAsAuthenticationType = signInAsType,
                AuthenticationType = "Google"
            };
            app1.UseGoogleAuthentication(options);
        }

        private static void ConfigureFacebookLogin(IAppBuilder app, string signInAsType)
        {
            FacebookSsoSettings facebookSsoSettings = SettingsGroupProvider.Current.Get<FacebookSsoSettings>(default(Guid?));
            if (!facebookSsoSettings.Enabled)
            {
                return;
            }

            string appId = facebookSsoSettings.AppId;
            string appSecret = facebookSsoSettings.AppSecret;
            if (appId.IsBlank() || appSecret.IsBlank())
            {
                return;
            }

            FacebookAuthenticationOptions authenticationOptions = new FacebookAuthenticationOptions();
            authenticationOptions.AppId = appId;
            authenticationOptions.AppSecret = appSecret;
            authenticationOptions.Caption = "Facebook";
            authenticationOptions.SignInAsAuthenticationType = signInAsType;
            authenticationOptions.AuthenticationType = "Facebook";
            authenticationOptions.Provider = new FacebookAuthenticationProvider()
            {
                OnAuthenticated = async context =>
                {
                    foreach (KeyValuePair<string, JToken> keyValuePair in context.User)
                    {
                        string key = keyValuePair.Key;
                        if (key == "first_name")
                        {
                            context.Identity.AddClaim(new Claim("given_name", keyValuePair.Value.ToString(), "XmlSchemaString", "Facebook"));
                        }
                        else
                        {
                            if (key == "last_name")
                            {
                                context.Identity.AddClaim(new Claim("family_name", keyValuePair.Value.ToString(), "XmlSchemaString", "Facebook"));
                            }
                        }
                    }

                    int num = await Task.FromResult(false) ? 1 : 0;
                }
            };
            authenticationOptions.BackchannelHttpHandler = new ConfigureIdentityServer.FacebookBackChannelHandler();
            authenticationOptions.UserInformationEndpoint = "https://graph.facebook.com/v2.8/me?fields=id,name,email,first_name,last_name";
            FacebookAuthenticationOptions options = authenticationOptions;
            options.Scope.Add("email");
            options.Scope.Add("public_profile");
            app.UseFacebookAuthentication(options);
        }

        private static void ConfigureWindowsAuthentication(IAppBuilder app, string signInAsType)
        {
        }

        private IdentityServerServiceFactory ConfigureCustomFactory(IdentityServerServiceFactory identityServerServiceFactory)
        {
            identityServerServiceFactory.ViewService = (Registration<IViewService>)new CustomViewServiceRegistration<CustomViewService>();
            return identityServerServiceFactory;
        }

        private class FacebookBackChannelHandler : HttpClientHandler
        {
            protected override async Task<HttpResponseMessage> SendAsync(
              HttpRequestMessage request,
              CancellationToken cancellationToken)
            {
                if (!request.RequestUri.AbsolutePath.Contains("/oauth"))
                {
                    request.RequestUri = new Uri(request.RequestUri.AbsoluteUri.Replace("?access_token", "&access_token"));
                }

                return await base.SendAsync(request, cancellationToken);
            }
        }
    }

Previous: Federated Authentication with Insite Identity Server - Part 1

Federated Authentication with Insite Identity Server - Part 1

My co-worker Nick Agnostopolus and I just went through a process of figuring out how to configure Sitecore Federated Authentication in 9.1.1 to use Insite Identity Server as an OpenID Connect provider. After a few long days we spent reflecting the Insite and Sitecore code, we have arrived at the solution. A few customizations had to be done on Insite side to make the whole thing work. Below is a detailed overview of what was done.

Configuring Insite Identity Provider in Sitecore

InsiteIdentity pipeline processor

First, a pipeline processor for Insite Identity had to be created. This is the class where most of the OpenID Connect magic happens.

    public class InsiteIdentity : IdentityProvidersProcessor
    {
        public const string PostLogoutEndPointName = "postinsitelogout";

        private readonly string oauthTokenEndpoint = "connect/token";

        private readonly string oauthUserInfoEndpoint = "connect/userinfo";

        public InsiteIdentity(
            FederatedAuthenticationConfiguration federatedAuthenticationConfiguration,
            ICookieManager cookieManager,
            BaseSettings settings)
            : base(federatedAuthenticationConfiguration, cookieManager, settings)
        {
        }

        public Collection<string> Scopes { get; } = new Collection<string>();

        protected override string IdentityProviderName => "InsiteIdentity";

        protected IdentityProvider IdentityProvider { get; set; }

        private string ClientId
        {
            get
            {
                return Settings.GetSetting("InsiteIdentity.ClientId", "Sitecore");
            }
        }

        private string ClientSecret
        {
            get
            {
                return Settings.GetSetting("InsiteIdentity.ClientSecret");
            }
        }

        private string Authority
        {
            get
            {
                return Settings.GetSetting("InsiteIdentity.Authority");
            }
        }

        private string CallbackAuthority
        {
            get
            {
                return Settings.GetSetting("InsiteIdentity.CallbackAuthority");
            }
        }

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

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

            args.App.UseOpenIdConnectAuthentication(this.CreateOpenIdConnectAuthenticationOptions(authenticationType, this.IdentityProvider));
        }

        protected string BuildPostLogoutRedirectUri(IOwinContext owinContext, string authority)
        {
            Assert.ArgumentNotNull((object)owinContext, nameof(owinContext));
            Uri uri = owinContext.Request.Uri;
            if (string.IsNullOrEmpty(authority))
            {
                authority = uri.GetLeftPart(UriPartial.Authority);
            }

            return InsiteIdentity.CombineUrlPath(authority, this.Settings.IdentityProcessingPathPrefix(), InsiteIdentity.PostLogoutEndPointName) + "?ReturnUrl" + "=" + HttpUtility.UrlEncode(uri.ToString());
        }

        private static string CombineUrlPath(params string[] parts)
        {
            List<string> list = ((IEnumerable<string>)parts).Where<string>((Func<string, bool>)(p => !string.IsNullOrEmpty(p))).ToList<string>();
            string str = string.Join("/", list.Select<string, string>((Func<string, string>)(p => p.Trim('/'))));
            if (list.First<string>()[0] == '/')
            {
                str = "/" + str;
            }

            if (list.Last<string>()[0] == '/')
            {
                str += "/";
            }

            return str;
        }

        private OpenIdConnectAuthenticationOptions CreateOpenIdConnectAuthenticationOptions(string authenticationType, IdentityProvider identityProvider)
        {
            var options = new OpenIdConnectAuthenticationOptions
            {
                Caption = this.IdentityProvider.Caption,
                AuthenticationType = authenticationType,
                AuthenticationMode = AuthenticationMode.Passive,
                ClientId = this.ClientId,
                ClientSecret = this.ClientSecret,
                Authority = this.Authority,
                ResponseType = OpenIdConnectResponseType.CodeIdTokenToken,
                Scope = string.Join(" ", (IEnumerable<string>)this.Scopes),
                UseTokenLifetime = false,
                CookieManager = this.CookieManager,

                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    SecurityTokenValidated = async notification =>
                    {
                        await SecurityTokenValidated(notification);
                    },
                    RedirectToIdentityProvider = notification =>
                    {
                        return RedirectToIdentityProvider(notification);
                    },
                }
            };

            options.TokenValidationParameters.SaveSigninToken = identityProvider.TriggerExternalSignOut;
            return options;
        }

        private async Task SecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            UserInfoResponse userInfoResponse;
            using (UserInfoClient userInfoClient = new UserInfoClient(notification.Options.Authority.EnsureTrailingSlash() + this.oauthUserInfoEndpoint))
            {
                LogManager.Diagnostic("[Sitecore Identity] auth_code: " + notification.ProtocolMessage.AccessToken, this);
                userInfoResponse = await userInfoClient.GetAsync(notification.ProtocolMessage.AccessToken, new CancellationToken()).ConfigureAwait(false);
            }

            var additionalClaims = new TransformationContextWithAdditionalClaims(this.FederatedAuthenticationConfiguration, this.IdentityProvider);
            if (userInfoResponse.IsError)
            {
                LogManager.Error("[Sitecore Identity] Unable to get user info - " + userInfoResponse.Error, userInfoResponse.Exception, this);
                LogManager.Diagnostic("[Sitecore Identity] userInfoResponse.Response.Raw: " + userInfoResponse.Raw, this);
            }
            else
            {
                additionalClaims.AdditionalClaims = userInfoResponse.Claims;
            }

            this.AddCustomClaims(notification, userInfoResponse);

            notification.AuthenticationTicket.Identity.ApplyClaimsTransformations((TransformationContext)additionalClaims);

            var transformationContext = new TransformationContextWithAdditionalClaims(this.FederatedAuthenticationConfiguration, this.IdentityProvider);
            foreach (Transformation transformation in (transformationContext.FederatedAuthenticationConfiguration?.SharedClaimsTransformations ?? Enumerable.Empty<Transformation>()).Concat((IEnumerable<Transformation>)transformationContext.IdentityProvider.Transformations))
            {
                transformation.Transform(notification.AuthenticationTicket.Identity, transformationContext);
            }
        }

        private void AddCustomClaims(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification, UserInfoResponse userInfoResponse)
        {
            var firstName = string.Empty;
            var lastName = string.Empty;

            ////Retrieve the claim given_name, and assign to first_name
            if (userInfoResponse.Claims.Any(k => k.Type.Contains("given_name")))
            {
                firstName = userInfoResponse.Claims.FirstOrDefault(k => k.Type.Contains("given_name")).Value;
            }

            ////The claim "family_name" is what was getting returned from the info
            if (userInfoResponse.Claims.Any(k => k.Type.Contains("family_name")))
            {
                lastName = userInfoResponse.Claims.FirstOrDefault(k => k.Type.Contains("family_name")).Value;
            }

            ////Add a custom claim, which is then transformed to the Sitecore FullName field.
            notification.AuthenticationTicket.Identity.AddClaim(new Claim("ExternalFullName", firstName + " " + lastName));

            ////Add another custom claim for comments:
            notification.AuthenticationTicket.Identity.AddClaim(new Claim("xComment", "Insite User"));

            notification.AuthenticationTicket.Identity.AddClaim(new Claim("insite_role", @"CommerceUsers\\Extranet User"));
        }

        private Task RedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            var owinContext = notification.OwinContext;

            OpenIdConnectMessage protocolMessage = notification.ProtocolMessage;

            if (protocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
            {
                protocolMessage.AcrValues = this.GetAcrValues(owinContext);
                protocolMessage.RedirectUri = this.BuildRedirectUri(owinContext, "signin");
                protocolMessage.Prompt = this.GetSuitablePrompt(owinContext, "login");
            }
            else if (protocolMessage.RequestType == OpenIdConnectRequestType.Logout && this.GetIdentityProvider().TriggerExternalSignOut)
            {
                protocolMessage.IdTokenHint = this.GetIdTokenHint(owinContext);
                protocolMessage.PostLogoutRedirectUri = this.BuildPostLogoutRedirectUri(owinContext, string.Empty);
            }

            return Task.CompletedTask;
        }
    }

An InsiteIdentityProvider class was implemented:


    public class InsiteIdentityProvider : DefaultIdentityProvider
    {
        public InsiteIdentityProvider(BaseDomainManager domainManager, BaseCorePipelineManager corePipelineManager) : base("InsiteIdentity", domainManager)
        {
            Assert.ArgumentNotNull((object)corePipelineManager, nameof(corePipelineManager));
            this.CorePipelineManager = corePipelineManager;
        }

        protected BaseCorePipelineManager CorePipelineManager { get; }
    }

Foundation.Account.config file

The following configurations were added to Foundation.Account.config file.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
    <sitecore>
        <sc.variable name="insiteIdentityServerAuthority" value="https://xxxinsite.local.com" />
        <settings>
            <setting name="InsiteIdentity.ClientId" value="xxx" />
            <setting name="InsiteIdentity.ClientSecret" value="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" />
            <setting name="InsiteIdentity.OAuthRedirectUri" value="/identity/externallogincallback" />
            <setting name="InsiteIdentity.Authority" value="$(insiteIdentityServerAuthority)/identity" />
            <setting name="InsiteIdentity.CallbackAuthority" value="/identity/externallogincallback" />
        </settings>
        <domainManager>
            <domains>
                <domain id="modules" type="Sitecore.Security.Domains.Domain, Sitecore.Kernel">
                    <param desc="name">$(id)</param>
                    <ensureAnonymousUser>false</ensureAnonymousUser>
                </domain>
            </domains>
        </domainManager>
        <pipelines>
            <accounts.loggedIn />
            <accounts.loggedOut />
            <accounts.registered />
            <owin.initialize>
                <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.Initialize.JwtBearerAuthentication, Sitecore.Owin.Authentication.IdentityServer"
                           patch:before="processor[@method='Authenticate']" resolve="true" >
                    <identityProviderName>InsiteIdentity</identityProviderName>
                    <audiences hint="raw:AddAudience">
                        <audience value="$(insiteIdentityServerAuthority)/resources"/>
                    </audiences>
                    <issuers hint="list">
                        <issuer>$(insiteIdentityServerAuthority)</issuer>
                    </issuers>
                </processor>
                <processor patch:before="*[@type='Sitecore.Owin.Authentication.Pipelines.Initialize.HandlePostLogoutUrl, Sitecore.Owin.Authentication']" mode="on" type="Xc.Foundation.Account.Infrastructure.Pipelines.Initialize.HandlePostLogoutUrl, Xc.Foundation.Account" resolve="true"/>
            </owin.initialize>
            <owin.identityProviders>
                <processor id="InsiteIdentity" type="Xc.Foundation.Account.Infrastructure.Pipelines.IdentityProviders.InsiteIdentity, Xc.Foundation.Account" resolve="true">
                    <scopes hint="list">
                        <scope name="openid">openid</scope>
                        <scope name="profile">profile</scope>
                        <scope name="iscapi">iscapi</scope>
                    </scopes>
                </processor>
            </owin.identityProviders>
        </pipelines>

        <federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">
            <!--Provider mappings to sites-->
            <identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
            </identityProvidersPerSites>

            <!--Definitions of providers-->
            <identityProviders hint="list:AddIdentityProvider">

                <identityProvider id="InsiteIdentity" type="Xc.Foundation.Account.Infrastructure.Providers.InsiteIdentityProvider, Xc.Foundation.Account" resolve="true">
                    <caption>Sign In</caption>
                    <domain>CommerceUsers</domain>
                    <enabled>true</enabled>
                    <triggerExternalSignOut>true</triggerExternalSignOut>

                    <transformations hint="list:AddTransformation">
                        <transformation name="apply additional claims" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.ApplyAdditionalClaims, Sitecore.Owin.Authentication.IdentityServer" resolve="true"/>
                        <transformation name="name to long name" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
                            <sources hint="raw:AddSource">
                                <claim name="preferred_username"/>
                            </sources>
                            <targets hint="raw:AddTarget">
                                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"/>
                            </targets>
                            <keepSource>true</keepSource>
                        </transformation>
                        <transformation name="role to long role" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
                            <sources hint="raw:AddSource">
                                <claim name="insite_role"/>
                            </sources>
                            <targets hint="raw:AddTarget">
                                <claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="CommerceUsers\Extranet User"/>
                            </targets>
                            <keepSource>false</keepSource>
                        </transformation>
                        <transformation name="set ShadowUser" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
                            <sources hint="raw:AddSource">
                                <claim name="http://schemas.microsoft.com/identity/claims/identityprovider" value="local"/>
                            </sources>
                            <targets hint="raw:AddTarget">
                                <claim name="http://www.sitecore.net/identity/claims/shadowuser" value="true"/>
                            </targets>
                            <keepSource>true</keepSource>
                        </transformation>
                        <!--
                            owin.cookieAuthentication.signIn pipeline uses http://www.sitecore.net/identity/claims/cookieExp claim to override authentication cookie expiration.
                                         'exp' claim value can be configured on Sitecore Identity server on the client configuration by IdentityTokenLifetimeInSeconds setting.
                                         Note: Claim value is Unix time expressed as the number of seconds that have elapsed since 1970-01-01T00:00:00Z 
                        -->
                        <transformation name="use exp claim for authentication cookie expiration" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
                            <sources hint="raw:AddSource">
                                <claim name="exp"/>
                            </sources>
                            <targets hint="raw:AddTarget">
                                <claim name="http://www.sitecore.net/identity/claims/cookieExp"/>
                            </targets>
                            <keepSource>true</keepSource>
                        </transformation>
                        <!--<transformation name="remove local role claims" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.RemoveLocalRoles, Sitecore.Owin.Authentication.IdentityServer"/>-->
                        <transformation name="adjust NameIdentifier claim" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.AdjustNameIdentifierClaim, Sitecore.Owin.Authentication.IdentityServer" resolve="true"/>
                    </transformations>
                </identityProvider>
            </identityProviders>

            <!--List of all shared transformations-->
            <sharedTransformations>
            </sharedTransformations>

            <!--Property mappings initializer-->
            <propertyInitializer type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication">
                <!--List of property mappings
                Note that all mappings from the list will be applied to each providers-->
                <maps hint="list">
                    <!--The mapping sets the Email property of the user profile from emailaddress claim-->
                    <map name="email claim" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
                        <data hint="raw:AddData">
                            <!--claim name-->
                            <source name="preferred_username" />
                            <!--property name-->
                            <target name="Email" />
                        </data>
                    </map>
                    <map name="first_name" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
                        <data hint="raw:AddData">
                            <source name="given_name" />
                            <target name="FirstName" />
                        </data>
                    </map>
                    <map name="full name" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
                        <data hint="raw:AddData">
                            <source name="ExternalFullName" />
                            <target name="FullName" />
                        </data>
                    </map>
                    <map name="comment" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
                        <data hint="raw:AddData">
                            <source name="xComment" />
                            <target name="Comment" />
                        </data>
                    </map>
                    <map name="role" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
                        <data hint="raw:AddData">
                            <source name="insite_role" />
                            <target name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" />
                        </data>
                    </map>
                </maps>
            </propertyInitializer>
        </federatedAuthentication>
    </sitecore>
</configuration>

Project.XXX.Website.config

In the Project layer, I added the following configurations:

        <federatedAuthentication  role:require="ContentDelivery or Standalone">
            <identityProvidersPerSites>
                <mapEntry name="atccorg" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication">
                    <sites hint="list">
                        <site>xxx</site>
                    </sites>
                    <identityProviders hint="list:AddIdentityProvider">
                        <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='InsiteIdentity']"  id="InsiteIdentity"  />
                    </identityProviders>
                    <externalUserBuilder type="Xc.Foundation.Account.Services.InsiteUserBuilder, Xc.Foundation.Account">
                        <param desc="isPersistentUser">true</param> 
                    </externalUserBuilder>
                </mapEntry>
            </identityProvidersPerSites>
        </federatedAuthentication>

InsiteUserBuilder.cs

User builder class had to be overwritten as well:

    public class InsiteUserBuilder : Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder
    {
        public InsiteUserBuilder()
        {
        }

        public InsiteUserBuilder(bool isPersistentUser) : base(isPersistentUser)
        {
        }

        public InsiteUserBuilder(string isPersistentUser) : base(isPersistentUser)
        {
        }

        public override ApplicationUser BuildUser(UserManager<ApplicationUser> userManager, ExternalLoginInfo externalLoginInfo)
        {
            ApplicationUser user = this.ApplicationUserFactory.CreateUser(this.CreateUniqueUserName(userManager, externalLoginInfo));
            user.IsVirtual = !this.IsPersistentUser;
            return user;
        }

        protected override string CreateUniqueUserName(UserManager<ApplicationUser> userManager, ExternalLoginInfo externalLoginInfo)
        {
            IdentityProvider identityProvider = this.FederatedAuthenticationConfiguration.GetIdentityProvider(externalLoginInfo.ExternalIdentity);
            if (identityProvider == null)
            {
                throw new InvalidOperationException("Unable to retrieve an identity provider for the given identity");
            }

            return identityProvider.Domain + "\\" + externalLoginInfo.ExternalIdentity.Name;
        }
    }

HandlePostLogoutUrl.cs

    public class HandlePostLogoutUrl : InitializeProcessor
    {
        private readonly BaseSettings settings;

        private readonly ExternalLogoutManager externalLogoutManager;

        public HandlePostLogoutUrl(BaseSettings settings, ExternalLogoutManager externalLogoutManager)
        {
            Assert.ArgumentNotNull((object)settings, nameof(settings));
            Assert.ArgumentNotNull((object)externalLogoutManager, nameof(externalLogoutManager));
            this.settings = settings;
            this.externalLogoutManager = externalLogoutManager;
        }

        public override void Process(InitializeArgs args)
        {
            if (!this.settings.FederatedAuthenticationEnabled())
            {
                return;
            }

            this.HandlePostExternalLogoutUrl(args);
        }

        private void HandlePostExternalLogoutUrl(InitializeArgs args)
        {
            args.App.Map(
                this.settings.IdentityProcessingPathPrefix().EnsureTrailingSlash() + InsiteIdentity.PostLogoutEndPointName,
                (Action<IAppBuilder>)(app => this.RunUnRegisterExternalLogout(app)));
        }

        private void RunUnRegisterExternalLogout(IAppBuilder app)
        {
            app.Run((Func<IOwinContext, Task>)(context => { this.UnRegisterExternalLogout(context); return Task.CompletedTask; }));
        }

        private void UnRegisterExternalLogout(IOwinContext owinContext)
        {
            Assert.ArgumentNotNull((object)owinContext, nameof(owinContext));
            owinContext.Response.Cookies.Delete(
                Sitecore.Owin.Authentication.Constants.ExternalLogOutCookieName, 
                new CookieOptions()
            {
                HttpOnly = true,
                Path = "/"
            });
            string str = owinContext.Request.Query["ReturnUrl"];
            if (string.IsNullOrEmpty(str) || WebUtil.IsExternalUrl(str, owinContext.Request.Uri.Host))
            {
                owinContext.Response.Redirect("/");
            }

            owinContext.Response.Redirect(str);
        }
    }

Site Definition

In the site definition the following attribute was added:

loginPage="$(loginPath)[xxxsitename]/InsiteIdentity"

Login Button

Controller Rendering:

 public ActionResult SignIn()
        {
            var url = !string.IsNullOrEmpty(Request.QueryString[Constants.QueryString.Url])
                        ? HttpUtility.UrlDecode(Request.QueryString[Constants.QueryString.Url])
                        : Request.RawUrl;

            var model = new LoginInfo
            {
                ReturnUrl = !string.IsNullOrEmpty(Request.QueryString[Constants.QueryString.ReturnUrl])
                    ? HttpUtility.UrlDecode(Request.QueryString[Constants.QueryString.ReturnUrl])
                    : url,
                LoginButton = this.fedAuthLoginRepository.GetAll().FirstOrDefault()
            };

            return this.View(model);
        }

View:

In the view responsible for rendering of the login button, the following code was added:

        if (Model.LoginButton != null)
        {
            <form action="@Model.LoginButton.Href" method="post" class="form-signin">
                <button class="btn btn-block btn-secondary" type="submit">
                    @if (!string.IsNullOrWhiteSpace(Model.LoginButton.IconClass))
                    {
                        <i class="@Model.LoginButton.IconClass"></i>
                    }
                    <span>
                        @Model.LoginButton.Caption
                    </span>
                </button>
            </form>
        }

LogOut

Controller action:

       [HttpGet]
        public ActionResult SignOut()
        {
            this.accountRepository.Logout();
            return this.Redirect("/");
        }

In AccountRepository we have the following:

        public bool LogOut()
        {          
            AuthenticationManager.Logout();
            return true;
        }

Above code and configurations force the user to be redirected to Insite Identity Server for authentication and log out.

Special Thanks to the following blogs: