Composable DXP Development MACH Quick Tips Sitecore Tips & Tricks

Authorization API for Sitecore Headless Services

The Sitecore Experience Platform is a product which can be utilized by small to large organizations to build enterprise-level applications.

In that manner, Sitecore Experience Platform is being utilized in many verticals e.g., Heath, Travel, Insurance, and any other verticals to build a process-oriented application which relies on the process defined in the specific verticals, and the application should adhere to those business processes.

One of the most important processes is Authorization.

From Wikipedia: Authorization is the function of specifying access rights/privileges to resources, which is related to general information security and computer security, and to access control in particular. More formally, “to authorize” is to define an access policy.

How to validate the access of a logged-in user on a Sitecore item in Sitecore Headless Implementation

To understand this, let’s assume that we have a website which implemented using Sitecore Headless Services and Front-end using NextJS Framework, and the website has restricted pages that only specific users can access who have access on the Sitecore CMS side.

Assume that access to Sitecore Item provided by Sitecore CMS by the Content Author or Business User’s on the basis of User Roles and I will not be covering the process of role assignment in this article.

If the user has access to the Sitecore Page item then render the page otherwise redirect to the Page Not Found page or another page.

Solution

To build a solution with restricted access, we should have a mechanism which will block or redirect users to a 404 page or access denied page.

In Sitecore Headless CMS-based implementation, we can handle access rights using the code behind so we have to rely on the Application Programming Interface (API) and Service Oriented Architecture (SOA) where all manipulation/business logic passed to Service Layer and Frontend only serve the content.

In the Sitecore Headless implementation whenever you will try to access any of the Sitecore Content from Sitecore then Sitecore Layout Service is called to get the published data for a specific page from Sitecore.

Credit: Sitecore

The below flow explains Sitecore Headless Development conceptual overview. It will help to understand how end-user request is being served for Sitecore Headless Services-based implementation:

Credit: Sitecore

In order to validate the user access on items, we can extend the Layout Service using the getLayoutServiceContext pipeline:

User request flow in Sitecore Headless Implementation

In the above flow, we are making a call to Layout service with Proxy Layer from the Front-end application so that the actual URL is not exposed to the outer world, and we can impose security on the CD app.

Codebase:

The below file will explain the logic to validate the requested Sitecore Item that it’s secure item or not on the basis of the Sitecore Item path passed to the Sitecore Layout service and return the output to the Front-end application with the Layout Service response that the user has access or not:

using CT.SC.Foundation.SitecoreExtensions.Extensions;
using Sitecore.Data.Managers;
using Sitecore.JavaScriptServices.Configuration;
using Sitecore.JavaScriptServices.ViewEngine.LayoutService.Pipelines.GetLayoutServiceContext;
using Sitecore.LayoutService.ItemRendering.Pipelines.GetLayoutServiceContext;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;

namespace CT.SC.Foundation.SitecoreExtensions.LayoutService.LayoutServiceContext
{
    public class UserItemAuthorization : JssGetLayoutServiceContextProcessor
    {
        private bool _isSecureRequest = false;
        private string _itemUrl = string.Empty;
        private const int Allowed = 1;
        public UserItemAuthorization(IConfigurationResolver configurationResolver) : base(configurationResolver)
        {
        }

        protected override void DoProcess(GetLayoutServiceContextArgs args, AppConfiguration application)
        {
            bool isSecureAccessEnabled = false;
            _isSecureRequest = this.IsSecureRequest(ref _itemUrl);

            isSecureAccessEnabled = this.VerifyUserAccessOnSecureItem();

            args.ContextData.Add("userAccess", new
            {
                enabled = isSecureAccessEnabled
            });
        }

        /// <summary>
        /// Is Layout service request for Secure Pages
        /// </summary>
        /// <param name="itemUrl">Get item Url</param>
        /// <returns>true/false</returns>
        private bool IsSecureRequest(ref string itemUrl)
        {
            bool isSecureRequest = false;
            // "item=/home/secure/item-1";
            var itemParam = HttpContext.Current.Request.Params["item"];
            string requestUrl = Convert.ToString(itemParam);
            if (!string.IsNullOrEmpty(requestUrl) && requestUrl.ToLower().Contains(Sitecore.Configuration.Settings.GetSetting(Constants.UserItemAuthorization.SecurePagesPath).ToLower()))
            {
                //-- If "/secure/" string present in the url and we don't want to execute validation if user landing 
                // to "item=/home/secure/"; secure area landing page
                if (requestUrl.IndexOf("/") == 0)
                {
                    var regex = new Regex(Regex.Escape("/"));
                    var newText = regex.Replace(requestUrl, "", 1);
                    requestUrl = newText.ToLower();
                }

                string[] urlDetails = requestUrl.Split(new string[] { Sitecore.Configuration.Settings.GetSetting(Constants.UserItemAuthorization.SecurePagesPath).ToLower() }, StringSplitOptions.None);
                if (urlDetails != null && urlDetails.Length >= 2)
                {
                    string securePagePath = urlDetails[1];
                    string[] securePageDetails = securePagePath.Split('/');
                    if (securePageDetails != null && securePageDetails.Length >= 1)
                    {
                        string itemName = securePageDetails[0];
                        // SC.Context.Site.ContentStartPath => "/sitecore/content/CT"
                        itemUrl = Sitecore.Context.Site.ContentStartPath + "/" + urlDetails[0] + Sitecore.Configuration.Settings.GetSetting(Constants.UserItemAuthorization.SecurePagesPath).ToLower() + itemName;
                        isSecureRequest = true;
                    }
                }

            }
            return isSecureRequest;
        }

        /// <summary>
        /// Get Sitecore Item Context from full URL with protocol and host
        /// </summary>
        /// <param name="url">Item url</param>
        /// <returns>Sitecore Item</returns>
        private Sitecore.Data.Items.Item GetSitecoreItemFromUrl(string url)
        {
            Sitecore.Data.Items.Item item = null;
            string path = new Uri(url).PathAndQuery;

            using (new Sitecore.SecurityModel.SecurityDisabler())
            {
                item = GetSitecoreItemFromPath(path);
            }
            return item;
        }

        /// <summary>
        /// Get Sitecore Item Context from the path after the hostname
        /// </summary>
        /// <param name="path">Item path</param>
        /// <returns>Sitecore Item</returns>
        private Sitecore.Data.Items.Item GetSitecoreItemFromPath(string path)
        {
            Sitecore.Data.Items.Item item = null;
            // remove query string
            if (path.Contains("?"))
                path = path.Split('?')[0];

            path = path.Replace(".aspx", "");

            using (new Sitecore.SecurityModel.SecurityDisabler())
            {
                item = Sitecore.Context.Database.GetItem(path);
            }

            return item;
        }

        /// <summary>
        /// Get Sitecore User
        /// </summary>
        /// <param name="domainName">User's domain name</param>
        /// <param name="userName">User's email address</param>
        /// <returns>Sitecore User</returns>
        private Sitecore.Security.Accounts.User GetUser(string domainName, string userName)
        {
            if (Sitecore.Security.Accounts.User.Exists(domainName + @"\" + userName))
            {
                return Sitecore.Security.Accounts.User.FromName(domainName + @"\" + userName, true);
            }
            return null;
        }

        /// <summary>
        /// Check user access on Secure Item
        /// </summary>
        /// <returns></returns>
        private bool VerifyUserAccessOnSecureItem()
        {
            bool isSecureAccessEnabled = false;

            if (_isSecureRequest && !string.IsNullOrEmpty(_itemUrl))
            {
                var userId = Convert.ToString(HttpContext.Current.Request.Params["userid"]);
                if (!string.IsNullOrEmpty(userId))
                {
                    Sitecore.Data.Items.Item secureItem = this.GetSitecoreItemFromPath(_itemUrl);

                    if (secureItem.IsDerived(Templates.SecureItem.ID))
                    {
                        //-- If it's Secure Item then only perform the check
                        Sitecore.Security.Accounts.User currentUser = this.GetUser(Sitecore.Configuration.Settings.GetSetting(Constants.UserItemAuthorization.Publishing.ExtranetDomain).ToLower(),
                            userId.ToLower());

                        if (currentUser != null && !string.Equals(currentUser.LocalName,
                            Sitecore.Configuration.Settings.GetSetting(Constants.UserItemAuthorization.Publishing.AnonymousUser), StringComparison.InvariantCultureIgnoreCase))
                        {
                            using (new Sitecore.Security.Accounts.UserSwitcher(currentUser))
                            {
                                //-- Change context to 'user' context to verify the access
                                if (
                                    (int)secureItem.Security.GetAccessRules().Helper.GetAccessPermission(currentUser, Sitecore.Security.AccessControl.AccessRight.ItemRead, Sitecore.Security.AccessControl.PropagationType.Entity) == Allowed
                                    &&
                                    (int)secureItem.Security.GetAccessRules().Helper.GetAccessPermission(currentUser, Sitecore.Security.AccessControl.AccessRight.ItemRead, Sitecore.Security.AccessControl.PropagationType.Descendants) == Allowed
                                    )
                                {
                                    isSecureAccessEnabled = true;
                                }
                            }
                        }

                    }
                }
            }

            return isSecureAccessEnabled;
        }
    }
}

You can access the complete code base at

Whenever you will make a call to Layout service from a Front-end application like:

https://cd.ct.localhost/sitecore/api/layout/render/default?item=/home/secure/secureitem-11&sc_apikey={e7d38e5e-bd7b-444b-a356-e3e2b1cf4a01}&sc_site=ctgwebsite&userid=amitkumar@example.com

Then Layout extension would be called which will return the value like:

Credit/References

Amit is an IT Solution Architect with Assurex. Reach out to Amit on LinkedIn

.


Did you find this useful? If so, please share with your connections.

Leave a Reply

Your email address will not be published. Required fields are marked *