Optimize API Calls for a Page with Multiple Components

Introduction

When working with a Product Page, it’s common for data to reside outside of Sitecore. In such cases, the CMS may only hold a reference ID to the actual data stored in another system. A typical page is composed of numerous components, and using getStaticProps for each component can result in multiple API calls, leading to inefficiency.

Solution

To address this issue, you can use a Page Props Factory Plugin. This plugin allows you to manage data fetching more efficiently by centralizing API calls.

Step-by-Step Guide

Create a New File

Navigate to /lib/page-props-factory/plugins and create a new file named product-props.ts.

Step 2: Add the Following Code


class ProductPropsPlugin implements Plugin {
  private productService: ProductService;

  order = 4;

  constructor() {
    this.productService = new ProductService();
  }

  async exec(props: SitecorePageProps) {
    if (!props.layoutData.sitecore.route) return props;
    if (props.layoutData.sitecore.route.templateId != PRODUCT_DETAIL_PAGE
      return props;

    const route = props.layoutData.sitecore.route;
    const productIdField = route.fields?.productId as Field<string>;
    if (productIdField.value === '') return props;
   
    const productResponse = await this.productService.getProduct(
      productIdField.value as string
    );

    const componentProps = props.componentProps as { [key: string]: unknown };
    componentProps['productData'] = productIdField.data.product;

    const errors = Object.keys(props.componentProps)
      .map((id) => {
        const component = props.componentProps[id] as ComponentPropsError;

        return component.error
          ? `\nUnable to get component props for ${component.componentName} (${id}): ${component.error}`
          : '';
      })
      .join('');

    if (errors.length) {
      throw new Error(errors);
    }

    return props;
  }
}

export const productPropsPlugin = new ProductPropsPlugin();

Step 3: Use the Property in Your Component

Finally, you can add the following code in your component to get the property:

const productData = useComponentProps<Product>('productData') as Product;

Conclusion

By implementing a Page Props Factory Plugin, you can optimize API calls for a page with multiple components. This approach reduces redundancy and improves performance by centralizing data fetching, making your application more efficient and maintainable.

Read more

XM Cloud – Docker container troubleshooting

Are you trying to up your docker instance and you noticed the following error, “curl: (6) Could not resolve host: nodejs.org”

There is an easy solution, how to fix this issue.

  1. Go To Docker Desktop -> Settings -> Docker Engine
  2. Add
{
  "dns": [
    "8.8.8.8"
  ],
 ....
}

DNS Record will help to your Docker to resolve the nodejs.org.

I hope that you find helpful this post.

Read more

Passing Attributes between Components within Placeholders in XM Cloud [NextJS]

Hey there! Have you ever found yourself scratching your head, wondering how to smoothly pass attributes from one component to another nestled inside Sitecore Placeholders? I recently delved into this topic, exploring the documentation provided by Sitecore (check it out here).

Despite diving into the docs, I couldn’t find a straightforward way to achieve this task. But fear not! I stumbled upon a hidden gem within the placeholder — the modifyComponentProps function.

 /**
     * Modify final props of component (before render) provided by rendering data.
     * Can be used in case when you need to insert additional data into the component.
     * @param {ComponentProps} componentProps component props to be modified
     * @returns {ComponentProps} modified or initial props
     */
 modifyComponentProps?: (componentProps: ComponentProps) => ComponentProps;

Excitedly, I decided to give it a whirl. However, to my dismay, it seemed like nothing happened, at least not with JSS v21.6.0.

After some pondering, I speculated that the ComponentPropsFactory might be overriding modifyComponentProps. But lo and behold, I found the information tucked away in ComponentPropsContext. And thus, a solution was born — a function to help you effortlessly achieve your goal.

All you need to do is install the following library:

npm i @constellation4sitecore/foundation-enhancers

Let me walk you through an example. Say you want to send properties to a child component inserted into a Placeholder.

const MyComponent = ({ fields, params, rendering }: MyComponentProps) => {
    // Invoke the magic
  useModifyChildrenProps(rendering, {
    myProp1: fields.myProp1,
    myProp2: 'hello world!',
  });

  return (
    <Placeholder
      name={`my-chindren-modules-${params.DynamicPlaceholderId}`}
      rendering={rendering}
    />
  );
};

And in your Children Component, you can effortlessly retrieve all the passed data.

type ChildComponentProps = {
    fields: Fields,
    // Add prop types here
    myProp1: Field<string>
    myProp2: string
}
const ChildComponent = ({ myProp1, myProp2 }: ChildComponentProps) => (
  <>
    <Text field={myProp1} />
    <span>{myProp2}</span>
  </>
);

This nifty solution has made passing data to all children a breeze. I hope you find this information as helpful as I did!

Common use cases:

  • You have a Tab component and need to send the active ID to children.
  • You need to re-render some props on specific breakpoints in the children component (e.g., mobile, desktop), and the parent component holds the information.

If you’re curious to dive deeper into this functionality and perhaps even contribute, you’re in luck! The function we discussed is part of an open-source project called Constellation 4 Sitecore. You can find the source code and contribute your ideas and improvements on Check it on Github!. We welcome collaboration from developers of all levels of expertise. Let’s join forces and make this tool even more powerful together! 🚀

Special thanks to Josue Jaramillo for invaluable assistance in troubleshooting the issue!

Read more

XM Cloud Next.js Practices for a Smooth Development

Working with Next.js application in XM Cloud, In this article I want to summarized what I learn until now.

Reserve fields only for Sitecore Fields

I do consider a good practice to reserve fields field only for Sitecore fields like the following example:

type ContentBlockProps = ComponentProps & {
  // Reserve fields for Sitecore Props
  fields: {
    heading: Field<string>;
    content: Field<string>;
  };
};

The reason that I consider a good practices is because in scenarios that you need to build a custom logic in getStaticProps, I feel that content fields are mixed with logic fields.

type ContentBlockProps = ComponentProps & {
  fields: {
    heading: Field<string>;
    content: Field<string>;
    myProp: MyType // prop not related with content
  };
};

export const getStaticProps: GetStaticComponentProps = async () => {
  const myCustomProp = await myLogic();
  return { fields: {
      myProp: myCustomProp 
   }
  };
};

So consider moving myprops out of fields.

type ContentBlockProps = ComponentProps & {
fields: {
    heading: Field<string>;
    content: Field<string>;
  };
 myProp: MyType // prop not related with content
};

When working with Variations try to reuse component

Per some investigations for name convention it’s a best practice to use PascalCase, however I need to find a way how to differenciate a React Componet vs a Sitecore Component.

So I suggest the following practice (maybe some day this becomes a best practice), anyway I’m open to feedback.

For example consider this scenario:

Simple Masthead with 3 Variations, Default, Color A, Color B.

So I starared to create a component with UI{ComponentName} so this means that this is a React Component that I can reuse it without necessary being a Sitecore Component.

const UISimpleMasthead = ({
 fields,
 variationParams}
}: SimpleMastheadProps): JSX.Element => 
(<>
Markup Here
</>);

Then I can declare the variations with the same approach

const UIDefault = (props: SimpleMastheadProps): JSX.Element => (
  <UISimpleMasthead {...props} variationParams={{ cssStyle: 'default' }} />
);
const UIColorA = (props: SimpleMastheadProps): JSX.Element => (
  <UISimpleMasthead {...props} variationParams={{ cssStyle: 'color-a' }} />
);
const UIColorB = (props: SimpleMastheadProps): JSX.Element => (
  <UISimpleMasthead {...props} variationParams={{ cssStyle: 'color-b' }} />
);

Finally export Sitecore Components / Variations as follows:

export const Default = withDatasourceRendering()<SimpleMastheadProps>(UIDefault);
export const ColorA = withDatasourceRendering()<SimpleMastheadProps>(UIColorA);
export const ColorB = withDatasourceRendering()<SimpleMastheadProps>(UIColorB);

With this approach I can reuse the markup and define the variations using props.

Also this could be used if you are working UI Components so you can Prefix your UI Components to understand that this is a UI and without prefix you know that this is a Sitecore Component.

I’ll continue writing more tips or suggestions to improve your developer experience. Happy coding!

Read more

Notes from a .Net Developer working with XM Cloud.

XM Cloud is a comprehensive Software-as-a-Service (SaaS) product designed to provide organizations with a powerful platform for managing their digital experiences. As a developer, you can start learning easily by fork the following project https://github.com/sitecorelabs/xmcloud-foundation-head.

To set up XM Cloud, please follow the instructions provided in the README file. One of the commands you need to run is the init command, which will generate keys for secure HTTPS connections. After running this command, you may notice that changes have been made to your .env file.

Please refer to the README file for detailed instructions on how to run the init command and this update your .env variables accordingly.

To complete the setup process, execute the up.ps1 script. This script will download the necessary files and create local Docker containers for XM Cloud. Running the script will ensure that all the required components are set up correctly and ready for use.

I’m taking by .NET developer persepective, venturing into the world of XM Cloud, there are a few key things you’ll need to learn and familiarize yourself with:

  • Be familiar with React Lifecycle. Understanding the React component lifecycle is essential for working with XM Cloud, as it is built on React and Next.js.
  • TypeScript: TypeScript is a statically-typed superset of JavaScript, and it’s beneficial to learn its syntax and features. W3Schools offers documentation and tutorials for TypeScript, providing a comprehensive resource to get you up to speed with the fundamentals.
  • Next.js Documentation: Next.js is the framework used in XM Cloud, and familiarizing yourself with its documentation is crucial. The Next.js documentation provides detailed information on concepts, features, and best practices for building applications with Next.js.
  • Sitecore Documentation: Sitecore, the company behind XM Cloud, provides valuable documentation and resources to help you ramp up on their platform.

By focusing on these resources, you’ll gain a solid foundation for working with XM Cloud.

Here are some observations and key points to keep in mind while working with Next.js and XM Cloud:

  • In the context of Sitecore MVC (Model-View-Controller), you typically use “Controller Rendering” to render dynamic content on web pages. Similarly, in Next.js, you can achieve similar functionality using “Server-Side Rendering (SSR)” or “Static Site Generation (SSG)” techniques by using a “Json Rendering” Template in Sitecore.
  • If you have been using TDS for Sitecore development, you can leverage Sitecore Serialization for Visual Studio as an alternative. It offers similar functionality to TDS, allowing you to manage Sitecore items using serialization. Sitecore Serialization for Visual Studio integrates with Visual Studio IDE and provides a user-friendly interface for working with serialized items.
  • Alternatively, you can use Sitecore Serialization from the command line, which offers a similar experience to Unicorn. Sitecore CLI integrates with Sitecore Serialization and provides commands for serialization tasks, allowing you to automate item serialization operations from the command line.
  • In traditional .NET Framework, the logic for retrieving data and preparing it for rendering is typically handled in controllers. However, in Next.js, you can achieve similar functionality by utilizing the getStaticProps function.
  • In Next.js, the term “model” is not commonly used as a specific concept. Instead, the focus is on React components and the data passed to these components as props. The props represent the data and behavior that are passed from a parent component to its child component. By defining the expected props fields, you establish a contract for the data that should be passed to the component.
  • The view is represented by the React components that define the structure and rendering logic of the user interface. These components are responsible for rendering data, managing local state, and defining the appearance and behavior of the UI elements.
  • In the Sitecore .NET framework, Sitecore.Context.Item is used to access the current item being processed. In contrast, in Next.js with Sitecore implementation, the equivalent would be accessing the route information through layoutData.sitecore.route. or sitecoreContext.route

Based on my learning experience, I’ve been working with Richard Cabral (developer of constellation4sitecore for .NET MVC Solution) in a framework that eliminates the complexities often associated with integrating Next.js and Sitecore (SXA), providing you with a smooth development experience.

This framework is in beta version and consist in the following features:

  • @constellation4sitecore/foundation-labels: It is an alternative of dictionaries, this helps you to consolidate all translatable texts for a component in one place.
  • @constellation4sitecore/foundation-data: This package helps you to access item information easily for example: getItem(itemId), derivedFrom(itemId, ‘BASE TEMPLATE ID’), getTemplateInfo(itemId) which perform graphql queries for you.
  • @constellation4sitecore/foundation-mapper: Foundation mapper contains helpers for maping fields based on Graphql structure to a props fields structure.
  • @constellation4sitecore/feature-navigation: A set of templates to easily build a navigation using Next.js. More examples comming up.

In the coming weeks, I will be posting more detailed documentation and helpful samples on how to effectively use our project. These resources will serve as valuable references for developers who are interested in leveraging our work. So, stay tuned for updates and get ready to dive into the exciting world of our project. Happy coding and exploring the possibilities it offers! If you have any questions or need assistance, please don’t hesitate to reach out.

Read more

Enable FEaaS Components in XM Cloud

Sitecore Components are designed to be modular, reusable, and customizable, allowing users to create websites quickly and easily by combining and configuring these components in different ways.

If you are trying Sitecore Components in early access and you started your xm cloud project before Sitecore Components, early access were released and when you try to add a component in your xm cloud site. You see an error that said: “The component cannot be dropped into this placeholder”. It is necessary that you update your solution from https://github.com/sitecorelabs/xmcloud-foundation-head

If you are starting a new project you can skip this quick tutorial, because FEaaSWrapper already exists into the starter kit.

Step 1: Verify that FEaaSWrapper.tsx exists under components folder

The easy way to fetch this files is to pull the latest from xmcloud-foundation-head (master) branch.

Otherwise, create a new file FEaaSWrapper.ts and copy the content with

https://github.com/sitecorelabs/xmcloud-foundation-head/blob/main/src/sxastarter/src/components/FEaaSWrapper.tsx

import { FEaaSComponent, FEaaSComponentProps } from '@sitecore-jss/sitecore-jss-nextjs';
import React from 'react';

export const Default = (props: FEaaSComponentProps): JSX.Element => {
  const styles = `component feaas ${props.params?.styles}`.trimEnd();
  const id = props.params?.RenderingIdentifier;

  return (
    <div className={styles} id={id ? id : undefined}>
      <div className="component-content">
        <FEaaSComponent {...props} />
      </div>
    </div>
  );
};

Step 2: Verify that you have FEaaS Available Renderings item

As I mentioned if your solution does not have FEaaS out-of-the-box it is probably that your solution is out-to-date.

To fix this you can right-click and add Available Renderings item and name it “FEaaS”

Finally all you need to do is add FEaaS Wrapper component as Available Rendering which is located in the following path: /sitecore/layout/Renderings/Foundation/JSS Experience Accelerator/FEaaS/FEaaS Wrapper

Right now if you go back to Sitecore Pages and try to add FEaaS Componet you are able to do it.

Read more

Troubleshooting: XDB Certificates on CD Server

It is very common that at some point your certificates expires and you need to renew your certificates. A common issues that I saw in projects is the following error:

The certificate was not found. Store: My, Location: LocalMachine, FindType: FindByThumbprint, FindValue: [YOUR-Thumprint-Value-Here], InvalidAllowed: False.
Source: Sitecore.Xdb.Common.Web

If you have a CM in one Server and CD server in another server. It is probably that you forgot to install the xconnect certificate in CD server.

You can run the following commands to validate if you have correctly installed the certificate for each server.

On CM Server run

Get-ChildItem  -Path Cert:\LocalMachine\MY | Where-Object {$_.Thumbprint -Match "[YOUR-Thumprint-Value-Here]"}

If you see a result then you can conclude that certificate is installed on CM server assuming that connect is installed in the same server.

Then you can check the same on CD server:

Get-ChildItem  -Path Cert:\LocalMachine\MY | Where-Object {$_.Thumbprint -Match "[YOUR-Thumprint-Value-Here]"}

If you noticed that you get no results, then you need to perform the following steps:

  1. Open Manage computer certificates in CM server.
  2. Go to Personal -> find the certificate that match your thumprint.
  3. Export Certificate

4. Move the file to the CD server

5. Install Certificate on Local Machine in Personal folder.

6. Grant Read access to the application app pool.

7. Recycle application pool and check your logs again.

Note: Be sure that thumbprint matches in CD server you can check the file ConnectionStrings.config to confirm that thumbprint is same as you certificate installed.

I hope that helps you to troubleshooting your certificates issues.

Read more

[Workaround] XM Cloud: Error building sample Next.js app

I started an XM Cloud project using Next.js as Rendering Host. I added some Graphql code, but when I want to run jss build I got an error similar to the next one. https://sitecore.stackexchange.com/questions/33141/error-during-build-of-sample-next-js-app

./src/components/graphql/GraphQL-ConnectedDemo.dynamic.tsx:114:72
Type error: Argument of type 'TypedDocumentNode<ConnectedDemoQueryQuery, Exact<{ datasource: string; contextItem: string; language: string; }>>' is not assignable to parameter of type 'string | DocumentNode'.
  Type 'TypedDocumentNode<ConnectedDemoQueryQuery, Exact<{ datasource: string; contextItem: string; language: string; }>>' is not assignable to type 'DocumentNode'.
    Types of property 'kind' are incompatible.
      Type '"Document"' is not assignable to type 'Kind.DOCUMENT'.

Environment

  • @sitecore-jss/sitecore-jss-nextjs: "^21.0.0"
  • @sitecore-jss/sitecore-jss-cli: "^21.0.0"
  • XM Cloud

After some research I found a workaround. I know this is not the best solution. But if someone else know how to fix this issue please, feel free to leave a comment.

Workaround

In order to stop the type error, I wrap the Graphql call into a js file. index.js

 export async function myFunctionToExecuteQuery
{
.... some code here
 const result = await graphQLClient.request(MyQueryDocument, {
    myParam: myParam,
  });

return result;
}

Then I added an index.d.ts to declare the types.

export declare async function myFunctionToExecuteQuery<TResult>(
  myParam: string
): Promise<TResult| null>;

With that workaround I can still declare the query result type, and also run jss build without any error.

As I mentioned, this is not the best solution, but I can build the Next.js application successfully.

Read more

Sitecore Single Sign-On: Federated Authentication With Azure AD

When you develop an enterprise website you might need to allow your visitors to login into your website to have a unique and personalized experience. Single Sign-On allows to your audience to enter credentials just one time instead of enter the password for each web application that your organization offers.

In the marketplace, there are many identity providers such as Google, Facebook, Microsoft, Github, etc. In enterprise environments you might have your own Azure Active Directory (Azure AD) which you store all your users information.

Sitecore provides a documentation which I found helpful to understand what needs to be configured in order to implement Federated Authentication for front-end login.

Sitecore Documentation:

https://doc.sitecore.com/xp/en/developers/102/sitecore-experience-manager/configure-federated-authentication.html

In this blog post I will explain Federated Authentication with Azure AD using OpenID Connect (OIDC) which is an authentication protocol based on the OAuth2 protocol. For more details please read the following article: https://learn.microsoft.com/en-us/azure/active-directory/fundamentals/auth-oidc

Configure an Identity Provider

First step, you need to create a config file for example, in my Sitecore instance I have a Foundation.Accounts project, so I created a file Foundation.Accounts.SSO.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">
     
    </federatedAuthentication>
  </sitecore>
</configuration>

Then inside federatedAuthentication we should add the following elements identityProvidersPerSites, identityProviders and propertyInitializer

identityProvidersPerSites add a MapEntry element which contains which sites the identity provider will be used. As I mentioned in this article, SSO will be used for website login.

In externalUserBuilder, I´m using a custom UserBuilder and IsPersistentUser is set to false. Because we don´t need to have unnecessary users in the CMS database. In other words, our users will be virtual users.

  <identityProvidersPerSites>
        <mapEntry name="ADMapEntry" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication" resolve="true">
          <sites hint="list">
            <site>website</site>
          </sites>
          <identityProviders hint="list:AddIdentityProvider">
            <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='AzureADIdentityProvider']" />
          </identityProviders>
          <externalUserBuilder type="Foundation.Accounts.Services.AzureActiveDirectory.ADExternalUserBuilder, Foundation.Accounts" resolve="true">
            <IsPersistentUser>false</IsPersistentUser>
          </externalUserBuilder>
        </mapEntry>
      </identityProvidersPerSites>
  public class ADExternalUserBuilder : DefaultExternalUserBuilder
    {
        public ADExternalUserBuilder(ApplicationUserFactory applicationUserFactory, IHashEncryption hashEncryption) : base(applicationUserFactory, hashEncryption) { }

        protected override string CreateUniqueUserName(UserManager<ApplicationUser> manager, ExternalLoginInfo externalLoginInfo)
        {
            if (externalLoginInfo == null) return "nullUserInfo";

            if (string.IsNullOrWhiteSpace(externalLoginInfo.Email))
            {
                var validUserName = externalLoginInfo.DefaultUserName;

                return $"{Constants.Domains.Extranet}\\{validUserName.Replace(" ", "")}";
            }

            return $"{Constants.Domains.Extranet}\\{externalLoginInfo.Email.Replace(" ", "")}";
        }
    }

Next section identityProviders is important because here we need to specify how claims are transformed for Sitecore Login and Business requirements. Notice that domain is extranet because our users are not part of the Sitecore domain.

<identityProviders hint="list:AddIdentityProvider">
        <identityProvider id="AzureADIdentityProvider" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication" >
          <param desc="name">$(id)</param>
          <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
          <caption>Go to login</caption>
          <domain>extranet</domain>
          <enabled>true</enabled>
          <triggerExternalSignOut>true</triggerExternalSignOut>
          <transformations hint="list:AddTransformation">
            <transformation name="Idp Claim" type="Sitecore.Owin.Authentication.Services.SetIdpClaimTransform, Sitecore.Owin.Authentication" />
            <transformation name="set id_token claim" type="Sitecore.Owin.Authentication.Services.SaveIdTokenInClaim, Sitecore.Owin.Authentication" />
            <transformation name="set role claim" type="Foundation.Accounts.Services.AzureActiveDirectory.SetRoleInClaim, Foundation.Accounts" />
            <transformation name="Name Identifier Claim" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" />
              </targets>
              <keepSource>false</keepSource>
            </transformation>
          </transformations>
        </identityProvider>
      </identityProviders>

For Azure AD Identity Provider I found that those 3 transformations are required. I created a custom transformation to resolve the user’s role as SetRoleInClaim.

<transformation name="Idp Claim" type="Sitecore.Owin.Authentication.Services.SetIdpClaimTransform, Sitecore.Owin.Authentication" />

<transformation name="set id_token claim" type="Sitecore.Owin.Authentication.Services.SaveIdTokenInClaim, Sitecore.Owin.Authentication" />

<transformation name="Name Identifier Claim" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" />
              </targets>
              <keepSource>false</keepSource>

Finally the last part is to intialize User variables like Email, Name, Last Name in propertyInitializer element.

 <propertyInitializer>
        <maps hint="list">
          <map name="email claim" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
            <data hint="raw:AddData">
              <!--claim name-->
              <source name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" />
              <!--property name-->
              <target name="Email" />
            </data>
          </map>
          <map name="Name claim" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
            <data hint="raw:AddData">
              <source name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" />
              <target name="Name" />
            </data>
          </map>
        </maps>
      </propertyInitializer>

Integrate with the owin.IdentityProviders Pipeline.

Next, you must integrate the code into the owin.identityProviders pipeline.

   <pipelines>
      <owin.identityProviders>
        <processor type="Foundation.Accounts.Services.AzureActiveDirectory.AzureADIdentityProviderProcessor, Foundation.Accounts" resolve="true" />
      </owin.identityProviders>
    </pipelines>

Add code for the provider

 public class AzureADIdentityProviderProcessor : IdentityProvidersProcessor
    {
        public AzureADIdentityProviderProcessor(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration, ICookieManager cookieManager, BaseSettings settings) : base(federatedAuthenticationConfiguration, cookieManager, settings)
        {
        }

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

            var identityProvider = this.GetIdentityProvider();
            var authenticationType = this.GetAuthenticationType();
            var saveSigninToken = identityProvider.TriggerExternalSignOut;

            var targetHostName = GetTargetHostName();
            string aadInstance = "https://login.microsoftonline.com/{0}/";
            string tenant = Settings.GetSetting("Foundation.Accounts.SSO.AD.Tenant");
            string clientId = Settings.GetSetting("Foundation.Accounts.SSO.AD.ClientId");
            string clientSecret = Settings.GetSetting("Foundation.Accounts.SSO.AD.ClientSecret");
            string postLogoutRedirectURI = $"{targetHostName}{Settings.GetSetting("Foundation.Accounts.SSO.AD.PostLogoutRedirectURI")}";
            string redirectURI = $"{targetHostName}{Settings.GetSetting("Foundation.Accounts.SSO.AD.RedirectURI")}";
            string scope = Settings.GetSetting("Foundation.Accounts.SSO.AD.Scope");

            string authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

            //Set the authentication type to use cookies
            args.App.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            });


            args.App.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                Caption = identityProvider.Caption,
                AuthenticationType = authenticationType,
                AuthenticationMode = AuthenticationMode.Passive,
                ClientId = clientId,
                ClientSecret = clientSecret,
                Authority = authority,
                PostLogoutRedirectUri = postLogoutRedirectURI,
                RedirectUri = redirectURI,
                ResponseType = OpenIdConnectResponseType.CodeIdToken,
                UseTokenLifetime = false,
                CookieManager = this.CookieManager,
                Scope = scope,
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    SecurityTokenValidated = notification =>
                    {
                        var identity = notification.AuthenticationTicket.Identity;
                        foreach (var claimTransformationService in identityProvider.Transformations)
                        {
                            claimTransformationService.Transform(identity,
                                new TransformationContext(FederatedAuthenticationConfiguration, identityProvider));

                        }

                        notification.AuthenticationTicket = new AuthenticationTicket(identity, notification.AuthenticationTicket.Properties);

                        return Task.FromResult(0);
                    },
                    RedirectToIdentityProvider = message =>
                    {
                        // format redirect URI so Sitecore cleans up after itself
                        var revokeProperties = message.OwinContext.Authentication.AuthenticationResponseRevoke?.Properties?.Dictionary;
                        if (revokeProperties != null && revokeProperties.ContainsKey("nonce"))
                        {
                            var uri = new Uri(message.ProtocolMessage.PostLogoutRedirectUri);
                            var host = uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
                            var path = "/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
                            var nonce = revokeProperties["nonce"];

                            // for single sign-out, Sitecore expects the URI used below with the nonce in the query string
                            // this URI was found in Sitecore.Owin.Authentication.Pipelines.Initialize.HandlePostLogoutUrl
                            message.ProtocolMessage.PostLogoutRedirectUri = $"{host}/identity/postexternallogout?ReturnUrl={path}&nonce={nonce}";
                        }

                        return Task.FromResult(0);
                    }

                },
                TokenValidationParameters =
                {
                    SaveSigninToken = saveSigninToken
                }
            });

    
        }

        protected override string IdentityProviderName => "AzureADIdentityProvider";
    }

This part is when I had a lot of issues, for me it is important to understand the settings. You can set up an enterprise application for Azure AD, then you can provide to the webapp the following fields: Tenant, Client Id and Client Secret.

Also is very important that you set up correctly the Redirect URL in your Azure AD Application, for example, in our case: https://sitecore-instance.dev/identity/externallogincallback.

Note:

Probably for .Net experts is obviously that redirect url will be something like targethostname + /identity/externallogincallback. However I spent a lot of time trying to figured out which is the callback url. Hopefully this article saves your time if this is your first ASP.NET Identity implementation.

SettingValue
Foundation.Accounts.SSO.AD.TenantAzure Tenant
Foundation.Accounts.SSO.AD.ClientIdAzure Application Client Id
Foundation.Accounts.SSO.AD.ClientSecretAzure Application Client Secret
Foundation.Accounts.SSO.AD.PostLogoutRedirectURI/identity/postexternallogout
Foundation.Accounts.SSO.AD.RedirectURI/identity/externallogincallback
Foundation.Accounts.SSO.AD.Scopeopenid profile roles

Generate sign-in links:

Finally, I created a AuthManager class in order to retrieved the sign-in link, where I can place it for example, in the header component.

 public SignInUrlInfo GetLoginUrl(string redirectUrl = "/")
        {
            var corePipelineManager =
                (BaseCorePipelineManager)Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService(typeof(BaseCorePipelineManager));
            var args = new GetSignInUrlInfoArgs(site: "website", returnUrl: redirectUrl);
            GetSignInUrlInfoPipeline.Run(corePipelineManager, args);
            var result = args.Result.FirstOrDefault();
            return result;
        }

Conclusion

To quickly recap, It is necessary to create a config file to place federatedAuthentication configurations and a processor in our case AzureADIdentityProviderProcessor to configure owin middleware. Then, you need to override ProcessCore method to add args.App.UseOpenIdConnectAuthentication configuration in IdentityProvidersProcessor implementation. Finally generate sign-in links and you are all set. It sounds easy but for me it took some days of research and trying to understand what values I need to include in the config files. I hope with this example you can save a lot of time and it could be helpful to understand this Sitecore feature.

Read more

Sitecore Geo IP Services (SC 10.2) Updated

Many businesses require to implements GEO IP Services. Business Rules differ between locations. As Developers, Sitecore makes our lives easier by providing a Service that we can use to obtain the location of the website’s visitor.

The IP Geolocation provides information like country, state, city, and every visitor’s registered company name as well. Based on geodata, we can build modules that interact with our visitors and provide accurate information depending on where they are.

Installation and Configuration

Here you will find the official link to activate and set up the Sitecore IP Geolocation: Set up Sitecore IP Geolocation

In your Siecore Solution ensure you add the following Nuget Packages:

  • Sitecore.Kernel
  • Sitecore.Analytics

Solution

Sometimes when you work with lower environments or your local environment Geo IP is not available. I developed the following simple class to get the visitor’s country and city. We can add the next file to setup a city and country code when Sitecore IP Geolocation is not available for your local. I used Sitecore 10.2.


<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:env="http://www.sitecore.net/xmlconfig/env/">
  <sitecore>
    <settings env:require="Local">
      <setting name="Dev.IPGeolocation.IsDevMode" value="true"/>
      <setting name="Dev.IPGeolocation.City" value="Boston"/>
      <setting name="Dev.IPGeolocation.CountryCode" value="US"/>
    </settings>
  </sitecore>
</configuration>

The next class contains a method GetUserGeolocation to get user location information.

Noticed IGeoIPManager is injected on the constructor class.

 public class GeolocationService : IGeolocationService
    {
        private readonly IGeoIpManager _geoIpManager;

        public GeolocationService(IGeoIpManager geoIpManager)
        {
            _geoIpManager = geoIpManager;
        }
        /// <summary>
        /// Get User IP form Request Context.
        /// </summary>
        /// <returns>(string) IP Address</returns>
        private static string GetUserIP()
        {
            var ip = (HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != null
                      && HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != "")
                ? HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"]
                : HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
            if (ip.Contains(","))
                ip = ip.Split(',').First().Trim();

            return ip.Trim();
        }

        /// <summary>
        ///   Fetch country from Sitecore IP Geolocation Service check 
        ///   setup at https://doc.sitecore.com/developers/93/sitecore-experience-manager/en/set-up-sitecore-ip-geolocation.html
        /// </summary>
        /// <returns></returns>
        public GeoData GetUserGeolocation()
        {
            GeoData geoData;
            bool IsDevEnviroment = Settings.GetBoolSetting("Dev.IPGeolocation.IsDevMode", false);
            if (!IsDevEnviroment)
            {
                try
                {
                    var result = _geoIpManager.GetGeoIpData(GetUserIP());
                    if (result.Status == GeoIpFetchDataStatus.Fetched &&
                        result.WhoIsInformation != null)
                    {
                        geoData = new GeoData()
                        {
                            City = result.WhoIsInformation.City,
                            CountryCode = result.WhoIsInformation.Country
                        };
                    }
                    else
                    {
                        throw new Exception("GeoIP whoIsInformation is null");
                    }
                }
                catch (Exception e)
                {
                    Log.Error("Error Getting IPAddress", e, typeof(GeolocationService));
                    if (Tracker.Current != null && Tracker.Current.Interaction != null && Tracker.Current.Interaction.GeoData != null)
                    {
                        geoData = new GeoData()
                        {
                            City = Tracker.Current.Interaction?.GeoData?.City,
                            CountryCode = Tracker.Current.Interaction?.GeoData?.Country
                        };

                    }
                    else
                    {
                        Log.Error("IPGeolocation: There is an issue with IP Geolocation Service", typeof(GeolocationService));
                        geoData = new GeoData()
                        {
                            City = null,
                            CountryCode = Settings.GetSetting("LocationEngine.IPGeolocation.CountryCode")
                        };
                    }
                }

            }
            else
            {
                Log.Info("IPGeolocation: Dev Mode is enabled", typeof(GeolocationService));
                geoData = new GeoData()
                {
                    City = Settings.GetSetting("Dev.IPGeolocation.City"),
                    CountryCode = Settings.GetSetting("Dev.IPGeolocation.CountryCode")
                };
            }
            Log.Info($"IPGeolocation: Country Code is {geoData.CountryCode}", typeof(GeolocationService));
            return geoData;
        }
    }

Finally, GeoData is a model class that contains the location information.


 public class GeoData
    {
        public string CountryCode { get; set; }
        public string City { get; set; }
    }

I hope this information will be usefully when you work with Sitecore Geo IP Services.

Read more