Sitecore 10 - Single Login Session and Kick Idle User

Recently, I received a security request from my client to maintain only a single session per account in Sitecore WCMS. If an account login on different devices, the previous login session will need to logout automatically.

Sitecore license is also taking count of concurrent login sessions, OOTB allow an user account to login to multiple devices. The licensing logic is pretty simple, which counting each user that logs into the Sitecore Client takes up a licensing slot (included same account), and you can't have more concurrent users than your license allows.

The simple way to prevent idle users take up the concurrent slots, we can run scheduler task with a simple script using DomainAccessGuard to kick idle users based on their last activity timestamp.

Eg. DomainAccessGuard.Kick(userSession.SessionID);

However, the above Kick method is only used to free up the slot from Sitecore license, not to logout a Sitecore user from the session and Sitecore user ticket remain valid and the next request user get logged in automatically and occupies free license slot.

In my case, I would like to maintain a session per account, which mean I will need to make sure that system will kick out the previous session from another device before allowing the session to access WCMS.

Solutions

Create a new processor to validate single session on <owin.cookieAuthentication.signedIn> pipeline, if same username already logged in on other device, we kick out the session then remove the user's ticket from Sitecore TicketManager

Create ValidateSingleSession class inherit from SignedInProcessor

using System;
using Sitecore.Abstractions;
using Sitecore.Owin.Authentication.Pipelines.CookieAuthentication.SignedIn;
using Sitecore.Web.Authentication;
using System.Collections.Generic;


namespace Sitecore.SitecoreExtensions.Owin.Authentication.Pipelines
{
    public class ValidateSingleSession : SignedInProcessor
    {
        protected BaseTicketManager TicketManager { get; }
        public ValidateSingleSession(BaseTicketManager ticketManager)
        {
            this.TicketManager = ticketManager;
        }
        public override void Process(SignedInArgs args)
        {
            if (!string.Equals(args.Site.Name, "shell", StringComparison.Ordinal))
                return;

            var validateSingleSession = Sitecore.Configuration.Settings.GetSetting("Security.ValidateSingleSession");
            if (!bool.Parse(validateSingleSession))
                return;

            List<DomainAccessGuard.Session> userSessionList = DomainAccessGuard.Sessions;

            if (userSessionList != null && userSessionList.Count > 0)
            {
                foreach (DomainAccessGuard.Session userSession in userSessionList.ToArray())
                {
                    if (args.User.UserName == userSession.UserName)
                    {
                        DomainAccessGuard.Kick(userSession.SessionID);
                        Sitecore.Diagnostics.Log.Audit($"Concurrent sessions detected: User {userSession.UserName} is kicked out ", this);
                    }
                }

            }

            //remove ticket 
            var ticketIds = this.TicketManager.GetTicketIDs();
            if (ticketIds != null && ticketIds.Count > 0)
            {
                foreach (var ticketID in ticketIds)
                {
                    var ticket = this.TicketManager.GetTicket(ticketID);
                    if (args.User.UserName == ticket.UserName)
                    {
                        this.TicketManager.RemoveTicket(ticketID);
                    }

                }
            }

        }
    }
}

Finally, we need to create a new config file to add processor to pipeline. The processor have to run before new ticket is issued, so that the removed ticket is always a ticket of previous session.

The pipeline to issue new ticket is Sitecore.Owin.Authentication.Pipelines.CookieAuthentication.SignedIn.CreateTicket

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <pipelines>
            <owin.cookieAuthentication.signedIn>
                <processor  patch:before="*[@type='Sitecore.Owin.Authentication.Pipelines.CookieAuthentication.SignedIn.CreateTicket, Sitecore.Owin.Authentication']"
                    type="Sitecore.SitecoreExtensions.Owin.Authentication.Pipelines.ValidateSingleSession, Sitecore.Foundation.SitecoreExtensions" resolve="true"/>
            </owin.cookieAuthentication.signedIn>
        </pipelines>
        <settings>
            <setting name="Security.ValidateSingleSession" value="true" />
        </settings>
    </sitecore>
</configuration>

I also created a field to allow development team to disable single session validation by introduce Security.ValidateSingleSession setting in config.