Redirect to login page if user is not logged in using Sitecore forms Authenctication Manager

 The scenario is as follows which covers all the cases in the code:

  1. When the user visits the page, check if user is at the /login page then keep him on login page if he is not logged in.
  2. When the user visits the page, and the user is trying to access the page other than /login and user is not logged in,  then redirect him to /login page.
  3. When user tries to login with credential and user doesn't exist then show error. And user can click on the Register button to redirect to register page.
  4. When user enters the correct credentials then redirect him to its specific country page (https://domain/countries/<country>) that user would have selected in the dropdown at the time of registration.

using Sitecore;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Links;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.Security.Authentication;
using Sitecore.Security.Domains;
using Sitecore.Web;
using System.Web;

namespace Feature.Forms.Pipelines
{
    public class CheckUserSession : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            var url = HttpContext.Current.Request.Url.AbsoluteUri.ToLower();
            //If Sitecore CMS URL then skip check
            if (url.Contains("sitecore") || Context.Site.IsBackend || Context.Site.Domain.Equals(Domain.GetDomain("sitecore")) || url.Contains("register"))
            {
                return;
            }
            //Here Home Page is login page.
            var loginPage = Context.Database.GetItem(Constant.LoginPage);
            var isLoginPage = loginPage.ID.Equals(Context.Item.ID);

            if (Context.Item != null)
            {
                //loginPage = no and authenicate = yes or //loginPage = yes and authenicate = no
                if ((!isLoginPage && Context.User.IsAuthenticated) || (isLoginPage && !Context.User.IsAuthenticated))
                    return;
                //loginPage = no and authenicate = no
                if (!isLoginPage && !Context.User.IsAuthenticated)
                    RedirectToLoginPage(loginPage);
                //loginPage = yes and authenicate = yes
                if (isLoginPage && Context.User.IsAuthenticated)
                    RedirectToItsCountryPage();
            }


        }
        #region Private Methods
        /// <summary>
        /// Check user session, if not valid redirect to configure page.
        /// </summary>
        private void RedirectToLoginPage(Item loginPage)
        {
            string redirectUrl = LinkManager.GetItemUrl(loginPage);
            RedirectUrl(redirectUrl);
        }
        /// <summary>
        /// If logged-in user trying to open url in browser different window/tab, user will redirect to landing/welcome page
        /// </summary>
        private void RedirectToItsCountryPage()
        {
            var user = AuthenticationManager.GetActiveUser();
            var countryBaseUrl = ((LinkField)(Context.Database.GetItem(Constant.CountryFolder)?.Fields[Constant.CountryPageField])).GetFriendlyUrl();
            var userCountryUrl = $"{HttpContext.Current.Request.Url?.Scheme}://{HttpContext.Current.Request.Url?.Host}{countryBaseUrl}/{user.Profile.GetCustomProperty(Constant.Country)}";
            RedirectUrl(userCountryUrl);
        }

        private void RedirectUrl(string url)
        {
            WebUtil.Redirect(url);
        }

        #endregion Private Methods
    }
}


I tried to test the code with the above mentioned points but still there may be some gaps (not major) please feel free to modify in your code repo. :-)

The Config file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" type="Feature.Forms.Pipelines.CheckUserSession, Feature.Forms" />
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Login user using Sitecore forms through Authentication Manager

 In this, we will check if the user exists and make him log in otherwise give an error. There is one Register button also that redirects to the Registration page.


The logic for login user is as:

using Foundation.Helper.Utility;
using Sitecore;
using Sitecore.Data.Fields;
using Sitecore.Diagnostics;
using Sitecore.ExperienceForms.Models;
using Sitecore.ExperienceForms.Processing;
using Sitecore.ExperienceForms.Processing.Actions;
using Sitecore.Security.Accounts;
using Sitecore.Security.Authentication;
using System.Linq;
using System.Web;

namespace Feature.Forms.FormActions
{
    public class LoginUser : SubmitActionBase<string>
    {
        public LoginUser(ISubmitActionData submitActionData) : base(submitActionData)
        {
        }

        protected override bool TryParse(string value, out string target)
        {
            target = string.Empty;
            return true;
        }

        protected override bool Execute(string data, FormSubmitContext formSubmitContext)
        {
            Assert.ArgumentNotNull(data, nameof(data));
            Assert.ArgumentNotNull(formSubmitContext, nameof(formSubmitContext));

            var fields = GetFormFields(data, formSubmitContext);

            Assert.IsNotNull(fields, nameof(fields));

            if (UsernameOrPasswordFieldIsNull(fields))
            {
                return AbortForm(formSubmitContext);
            }

            var user = Login(fields.Username, fields.Password);

            if (user == null)
            {
                return AbortForm(formSubmitContext);
            }

            //Redirect to external URL
            var countryBaseUrl = ((LinkField)(Context.Database.GetItem(Constant.CountryFolder)?.Fields[Constant.CountryPageField])).GetFriendlyUrl();
            var userCountryUrl = $"{HttpContext.Current.Request.Url?.Scheme}://{HttpContext.Current.Request.Url?.Host}{countryBaseUrl}/{user.Profile.GetCustomProperty(Constant.Country)}";

            formSubmitContext.RedirectUrl = userCountryUrl;
            formSubmitContext.RedirectOnSuccess = true;

            return true;
        }

        protected virtual User Login(string userName, string password)
        {
            var accountName = string.Empty;
            var domain = Context.Domain;
            if (domain != null)
            {
                accountName = domain.GetFullName(userName);
            }

            var result = AuthenticationManager.Login(accountName, password);
            if (!result)
            {
                return null;
            }

            var user = AuthenticationManager.GetActiveUser();
            return user;
        }

        private LoginUserFormFields GetFormFields(string data, FormSubmitContext formSubmitContext)
        {
            Assert.ArgumentNotNull(data, nameof(data));
            Assert.ArgumentNotNull(formSubmitContext, nameof(formSubmitContext));

            return new LoginUserFormFields
            {
                Username = Helper.GetValue(formSubmitContext.Fields.FirstOrDefault(f => f.Name.Equals("Username"))),
                Password = Helper.GetValue(formSubmitContext.Fields.FirstOrDefault(f => f.Name.Equals("Password")))
            };
        }

        private bool UsernameOrPasswordFieldIsNull(LoginUserFormFields field)
        {
            Assert.ArgumentNotNull(field, nameof(field));
            return field.Username == null || field.Password == null;
        }

        private bool UsernameOrPasswordValueIsNull(LoginUserFieldValues values)
        {
            Assert.ArgumentNotNull(values, nameof(values));
            return string.IsNullOrEmpty(values.Username) || string.IsNullOrEmpty(values.Password);
        }

        private bool AbortForm(FormSubmitContext formSubmitContext)
        {
            formSubmitContext.Abort();
            return false;
        }

        internal class LoginUserFormFields
        {
            public string Username { get; set; }
            public string Password { get; set; }

            public LoginUserFieldValues GetFieldValues()
            {
                return new LoginUserFieldValues
                {
                    Username = Helper.GetValue(Username),
                    Password = Helper.GetValue(Password)
                };
            }
        }

        internal class LoginUserFieldValues
        {
            public string Username { get; set; }
            public string Password { get; set; }
        }
    }
}

using System.Collections.Generic;

namespace Foundation.Helper.Utility
{
    public class Helper
    {
        public static string GetValue(object field)
        {
            return field?.GetType().GetProperty("Value")?.GetValue(field, null)?.ToString() ?? string.Empty;
        }
        public static string GetDropdownValue(object field)
        {
            var value = field?.GetType().GetProperty("Value")?.GetValue(field, null);
            return ((List<string>)value)[0] ?? string.Empty;
        }
    }
}


Registration form using Sitecore forms

We can use the below code to create the registration for the user. I have created the following fields:

1. Full Name - Single Line Text

2. Email - Email

3. Password & ConfirmPassword - Confirm Password

4. Country - Dropdown

5. Submit button - submit

we will attach an action on the submit button that will execute the logic and register this user.

We are saving the form in the Sitecore forms DB.

using Foundation.Helper.Utility;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.ExperienceForms.Models;
using Sitecore.ExperienceForms.Processing;
using Sitecore.ExperienceForms.Processing.Actions;
using Sitecore.Security.Accounts;
using System;
using System.Linq;

namespace Feature.Forms.FormActions
{
    public class Registration : SubmitActionBase<string>
    {
        public Registration(ISubmitActionData submitActionData) : base(submitActionData)
        {
        }

        protected override bool TryParse(string value, out string target)
        {
            target = string.Empty;
            return true;
        }

        protected override bool Execute(string data, FormSubmitContext formSubmitContext)
        {
            Assert.ArgumentNotNull(data, nameof(data));
            Assert.ArgumentNotNull(formSubmitContext, nameof(formSubmitContext));

            var fields = GetFormFields(formSubmitContext);

            Assert.IsNotNull(fields, nameof(fields));

            if (EmailOrPasswordFieldsIsNull(fields))
            {
                return AbortForm(formSubmitContext);
            }

            var result = Register(fields.Email, fields.Password, fields.FullName, Guid.NewGuid().ToString(), fields.Country);

            if (!result)
            {
                return AbortForm(formSubmitContext);
            }

            return true;
        }

        protected virtual bool Register(string email, string password, string name, string profileId, string country)
        {
            Assert.ArgumentNotNullOrEmpty(email, nameof(email));
            Assert.ArgumentNotNullOrEmpty(password, nameof(password));

            try
            {
                var user = User.Create(Context.Domain.GetFullName(email), password);
                user.Profile.Email = email;

                if (!string.IsNullOrEmpty(profileId))
                {
                    user.Profile.ProfileItemId = profileId;
                    user.Profile.SetCustomProperty(Constant.Country, country);
                }

                user.Profile.FullName = name;
                user.Profile.Save();
            }
            catch (Exception ex)
            {
                Log.SingleError("Register user failed", ex);
                return false;
            }

            return true;
        }

        private RegisterUserFormFields GetFormFields(FormSubmitContext formSubmitContext)
        {
            Assert.ArgumentNotNull(formSubmitContext, nameof(formSubmitContext));

            return new RegisterUserFormFields
            {
                Email = Helper.GetValue(formSubmitContext.Fields.FirstOrDefault(f => f.Name.Equals("Email"))),
                Password = Helper.GetValue(formSubmitContext.Fields.FirstOrDefault(f => f.Name.Equals("PasswordConfirmation"))),
                FullName = Helper.GetValue(formSubmitContext.Fields.FirstOrDefault(f => f.Name.Equals("FullName"))),
                Country = Helper.GetDropdownValue(formSubmitContext.Fields.FirstOrDefault(f => f.Name.Equals("Country")))
            };
        }

        private bool EmailOrPasswordFieldsIsNull(RegisterUserFormFields field)
        {
            Assert.ArgumentNotNull(field, nameof(field));
            return field.Email == null || field.Password == null;
        }

        private bool EmailOrPasswordsIsNull(RegisterUserFieldValues values)
        {
            Assert.ArgumentNotNull(values, nameof(values));
            return string.IsNullOrEmpty(values.Email) || string.IsNullOrEmpty(values.Password);
        }

        private bool AbortForm(FormSubmitContext formSubmitContext)
        {
            formSubmitContext.Abort();
            return false;
        }

        internal class RegisterUserFormFields
        {
            public string Email { get; set; }
            public string Password { get; set; }
            public string FullName { get; set; }
            public string Country { get; set; }

            public RegisterUserFieldValues GetFieldValues()
            {
                return new RegisterUserFieldValues
                {
                    Email = Helper.GetValue(Email),
                    Password = Helper.GetValue(Password),
                    FullName = Helper.GetValue(FullName),
                    Country = Helper.GetValue(Country)
                };
            }
        }

        internal class RegisterUserFieldValues
        {
            public string Email { get; set; }
            public string Password { get; set; }
            public string FullName { get; set; }
            public string Country { get; set; }
        }
    }

}

using System.Collections.Generic;

namespace Foundation.Helper.Utility
{
    public class Helper
    {
        public static string GetValue(object field)
        {
            return field?.GetType().GetProperty("Value")?.GetValue(field, null)?.ToString() ?? string.Empty;
        }
        public static string GetDropdownValue(object field)
        {
            var value = field?.GetType().GetProperty("Value")?.GetValue(field, null);
            return ((List<string>)value)[0] ?? string.Empty;
        }
    }
}

The form looks like:






Sitecore roles for various kind of authors

 Content author with read access only and section like PowerShell, reporting tools, template manager

sitecore\Sitecore Client Users
sitecore\Designer
sitecore\Author

with this, the content tree and editing option will look like something:



 Content author with read access only and with Experience Analytics access.

sitecore\Sitecore Client Users
sitecore\Designer
sitecore\Author
sitecore\Developer

with this, the content tree and editing option will look like something:







Content author with read access only but with few items to be editable.

sitecore\Sitecore Client Users
sitecore\Designer
sitecore\Author

1. assign these roles to the new author.
2. Now go the security editor (by admin account), and ensure you have selected the author in the account box as shown in screenshot.

3. Now I wanted the author to edit the countries item so I checked the write and if you want its children also to be edited then select inheritance on parent (countries) so that the same permission can be inherited on the children.
 


Wildcard page in Sitecore - using rendering datasource resolver

 I hope you would have seen the article already regarding the wildcard using item resolver before reading this.

So, to resolve the wildcard using the data source resolver the idea is that this time we will apply the rendering, or presentation detail on the * item instead of the data source item unlike shown in the previous blog.

So our structure will be now as follows:

- Home
    - Countries
                    * (with presentation detail with your controller rendering available)

we will keep the data items in the global folder as

- Data
    - Countries
        India - (with no presentation details)
        USA - (with no presentation details)
        Russia - (with no presentation details)

Now, when URL URL https://[domain]/countries/india maps to the * item then we will run a processor to resolve the datasource dynamically on the basis of country passed in the URL.

So, in processor we will say that in the rendering dynamically pass the datasource as /sitecore/content/POC Site/New site/Data/Countries/{last segment of URL}. Here in the example last segment of the URL will be India so the complete datasource on runtime will be /sitecore/content/POC Site/New site/Data/Countries/india.

Here is the code below:

using Sitecore.Mvc.Pipelines.Response.GetRenderer;
using System;
using System.Linq;

namespace Feature.WeatherForecast.Pipelines
{
    public class CountryDatasource : GetRendererProcessor
    {
        private string RenderingName { get; set; }
        public override void Process(GetRendererArgs args)
        {
            var productsDetailRendering = RenderingName;
            if (args.PageContext.Item.Name != "*"
                || !args.Rendering.RenderingItem.Name.Equals(productsDetailRendering, StringComparison.InvariantCultureIgnoreCase))
                return;

            if (args.PageContext.RequestContext.HttpContext.Request.Url == null) return;

            const string datasourceFolder = "/sitecore/content/POC Site/New site/Data/Countries";
            var dataSourcePath =
                $"{datasourceFolder}/{args.PageContext.RequestContext.HttpContext.Request.Url.Segments.Last()}";

            var dataSourceItem = Sitecore.Context.Database.GetItem(dataSourcePath);
            if (dataSourceItem != null)
            {
                args.Rendering.DataSource = dataSourcePath;
            }
            else
            {
                var notFoundItem =                     Sitecore.Context.Database.GetItem("/sitecore/content/YourSite/NotFoundPage");

                if (notFoundItem != null)
                {
                    // Set the 404 page item as the current item
                    Context.Item = notFoundItem;
                }
                else
                {
                    // Log a warning if the 404 page item is not found
                    Log.Warn("404 Page Not Found item not found in Sitecore content tree.", this);
                }
            }
        }
    }
}
Finally patch your changes:


<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.getRenderer>
        <processor type="Feature.WeatherForecast.Pipeliness.CountryDatasource, Feature.WeatherForecast" patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.GetRenderer.GetViewRenderer, Sitecore.Mvc']">
          <renderingname>WeatherForecast</renderingname>
        </processor>
      </mvc.getRenderer>
    </pipelines>
  </sitecore>
</configuration>

Hope it helps!

Wildcard page in Sitecore - Using Item resolver

There are several ways of implementing the wildcard pages. A wildcard item in Sitecore is a way to create dynamic URLs that pass data through the URL instead of relying on query string values. The name of a wildcard item must be the wildcard character (*), and it matches any item name on the same level as the wildcard item. For example, if the product name is “product-1”, its URL would be http://domain/products/product-1.

So directly coming to the point. I personally used 2 ways of handling the wildcard pages.

  1. Using item resolver, or
  2. Using rendering data source resolver.

Using item resolver


In this, you create a wildcard item as:

- Home
    - Countries
                    * (with no rendering on it, only layout will be available)

Now when you request a URL as https://[domain]/countries/india or https://[domain]/countries/usa or https://[domain]/countries/russia, then it should work.

So idea is that we will keep the data items in the global folder as

- Data
    - Countries
        India - (with presentation detail having the controller rendering, e.g., forecast rendering)
        USA - (with presentation detail having the controller rendering, e.g., forecast rendering)
        Russia - (with presentation detail having the controller rendering, e.g., forecast rendering)

Now, URL https://[domain]/countries/india maps to the * item, and 
We will write a pipeline that will set the context.item as India (available under the Data/Countries folder). 
Since this item has the controller rendering available, that item will be resolved to present on the front end.

So here is the code

using System;
using System.Linq;
using System.Net;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;

namespace Feature.WeatherForecast.Pipelines
{
    public class WildcardItemResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            if (args.Url.ItemPath.StartsWith("/sitecore/content/POC Site/New site/Home/Country", StringComparison.OrdinalIgnoreCase))
            {
                // Extract wildcard parameter from URL
                string wildcardValue = ExtractWildcardValue(args.HttpContext.Request.Url);

                if (!string.IsNullOrEmpty(wildcardValue))
                {
                    // Resolve the wildcard value to a specific Sitecore item
                    var item = ResolveWildcardItem(args, wildcardValue);
                    if (item != null)
                    {
                        // Set the resolved item in the Sitecore context
                        Context.Item = item;
                    }
                    else
                    {
                        Item errorPage = args.GetItem("/sitecore/content/POC Site/New site/Home/404");
                        if (errorPage != null)
                        {
                            args.ProcessorItem = errorPage;
                            Context.Item = errorPage;
                            Context.Items["httpStatus"] = HttpStatusCode.NotFound;
                            args.HttpContext.Response.TrySkipIisCustomErrors = true;
                        }
                        // Optionally handle when the wildcard value doesn't resolve to any item
                        Log.Warn($"Wildcard item not found for value '{wildcardValue}'", this);
                    }
                }
            }
        }

        private string ExtractWildcardValue(Uri url)
        {
            // Extract wildcard value from the URL path
            return url.Segments.Last();
        }

        private Item ResolveWildcardItem(HttpRequestArgs args, string wildcardValue)
        {
            // Implement logic to resolve wildcard value to a specific Sitecore item
            var wildcardFolderPath = "/sitecore/content/POC Site/New site/Data/Countries";
            return args.GetItem($"{wildcardFolderPath}/{wildcardValue}");            
        }
    }
}

Finally patch your changes as:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" type="Feature.WeatherForecast.Pipelines.WildcardItemResolver, Feature.WeatherForecast" />
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

If it doesn't work after this then reason can be following:

<mvc.getPageItem patch:source="Sitecore.Mvc.config">
  <processor type="Sitecore.Mvc.Pipelines.Response.GetPageItem.SetLanguage, Sitecore.Mvc"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetPageItem.GetFromRouteValue, Sitecore.Mvc"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetPageItem.GetFromRouteUrl, Sitecore.Mvc"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetPageItem.GetFromOldContext, Sitecore.Mvc"/>
  <processor type="Sitecore.ContentTesting.Mvc.Pipelines.Response.GetPageItem.PageLevelTestVariantResolver, Sitecore.ContentTesting.Mvc" patch:source="Sitecore.ContentTesting.Mvc.config"/>
  <processor type="Sitecore.ContentTesting.Mvc.Pipelines.Response.GetPageItem.ContentTestVariantResolver, Sitecore.ContentTesting.Mvc" patch:source="Sitecore.ContentTesting.Mvc.config"/>
</mvc.getPageItem>

The processor GetFromOldContext will set the PageItem to Context.Item only if the previous processors haven't already identified an item.

Recommedation

Create a processor for this pipeline that comes after either GetFromRouteUrl or GetFromOldContext to determine whether you want to continue using the Page Item found, or use from the Custom Item Resolver.

Or

The really simple solution to resolve this issue is to reorder the processors within the mvc.getPageItem pipeline so that GetFromOldContext always comes after the SetLanguage processor.

<sitecore>
  <pipelines>
    <mvc.getPageItem>
      <processor type="Sitecore.Mvc.Pipelines.Response.GetPageItem.GetFromOldContext, Sitecore.Mvc">
        <patch:delete />
      </processor>
      <processor patch:after="processor\[@type='Sitecore.Mvc.Pipelines.Response.GetPageItem.SetLanguage, Sitecore.Mvc'\]" type="Sitecore.Mvc.Pipelines.Response.GetPageItem.GetFromOldContext, Sitecore.Mvc"/>
    </mvc.getPageItem>
  </pipelines>
</sitecore>

A little more involved solution is to override the PageContext to fix the issue; to do that we need to override the SetupPageContext processor to add our custom PageContext

namespace YourNamespace
{
    using System.Web.Routing;
    using Sitecore.Data.Items;
    using Sitecore.Mvc.Pipelines.Request.RequestBegin;
    using Sitecore.Mvc.Presentation;

    public class SetupPageContext : Sitecore.Mvc.Pipelines.Request.RequestBegin.SetupPageContext
    {
        protected override PageContext CreateInstance(RequestContext requestContext, RequestBeginArgs args)
        {
            return new PageContextFixed
            {
                RequestContext = requestContext
            };
        }
    }

    public class PageContextFixed : PageContext
    {
        protected override Item GetItem()
        {
            // Assumption: If you have a Context.Item you have to have a Context.Language
            return Sitecore.Context.Item ?? base.GetItem();
        }
    }
}

And the configuration:

<sitecore>
  <pipelines>
    <mvc.requestBegin>
      <processor patch:instead="processor\[@type='Sitecore.Mvc.Pipelines.Request.RequestBegin.SetupPageContext, Sitecore.Mvc'\]" type="Example.SetupPageContext,Example" />
    </mvc.requestBegin>
  </pipelines>
</sitecore>


The information credit goes to the following:

Now let's understand the same scenario for wildcard page with datasource resolver.



Creating Solr core for sitecore using command prompt

We setup the solr cores using command “C:\solr8985\sc103solr-8.11.2\bin>solr.cmd create -c sitecore_sxa_web_index -d sitecore_configset” ...