DEV Community

Cover image for Sitecore integration with Azure Active Directory B2C
Byteminds Agency for ByteMinds

Posted on • Originally published at byteminds.co.uk

Sitecore integration with Azure Active Directory B2C

We would like to share our experience of integrating Sitecore 9.3 with the Azure AD B2C (Azure Active Directory Business to Consumer) user management system. We will describe how we successfully built a personal account using these technologies, the challenges we encountered, and how we ultimately resolved them.

Introduction

Sitecore CMS is a prominent content management system built on the .NET Framework. According to Built With, over 17,000 websites have been developed using Sitecore, including those of Microsoft, United Airlines, PUMA, L'Oreal, and others. Sitecore is a key component of our technology stack. In collaboration with our partners UNRVLD (formerly Delete Agency), we have developed sites on Sitecore for various companies and brands such as Biffa, Leeds Beckett University, The Open, Southampton FC, Royal Canin, and many more.

Azure Active Directory B2C is a cloud-based customer identity access management service that enables users to sign in to applications using social media, enterprise, and local accounts.

Project Objective

For one of our projects, we faced the task of creating a personalized account for clients of a large organization. All users of this account should have the ability to view current transactions, generate invoices for payments, communicate with technical support, manage personal information, etc. Additionally, all data within the personal account needed to be fetched securely from our customer's corporate system via an API. Azure Active Directory B2C was chosen as the user entry point by the customer.

Let's take a closer look at the challenges we encountered and the solutions we developed:

  • Integrating with a secure API provided by the customer
  • Implementing user registration
  • Implementing user login/logout for the personal account, as well as password reset functionality
  • Implementing impersonation, enabling administrators and technical specialists from the customer's team to access user accounts without knowing their login credentials.

We encountered several difficulties during this process, including:

  • Implementing support for multiple user functions (login, impersonation, registration, password reset) within the website
  • Simultaneously logging out of Azure AD B2C and Sitecore
  • Redirecting users to the Azure AD B2C login page during login
  • Preventing an increase in the number of authentication cookies, which could lead to exceeding the allowed request length.

While the internet offers extensive documentation, articles, and guides on Sitecore CMS and Azure Active Directory B2C, we were unable to find solutions tailored to our specific problems and challenges consolidated into a single article. Consequently, we had to address these issues ourselves and occasionally sought assistance from Sitecore's official technical support. In this article, we will share the knowledge gained from our experiences.

Creation of an Azure AD B2C Client

To replicate our Sitecore integration with Azure AD B2C, we need to establish a basic Azure AD B2C client. This client should include standard sign-up, sign-in, and password reset user flows. Follow the official guide, replicating steps 1 to 3, to create a functioning client with the desired user flow configurations.

Image description

We must also select the claims to be included in the ID token. In addition to standard claims like name and email, we'll add a "Roles" claim. To do this, click on Manage user attributesAdd. This will allow us to assign roles such as administrator, client, or guest when creating a user. Keep in mind that in a real application, this would require configuring individual user policies. However, the approach described above is sufficient for demonstration purposes.

Image description

Subsequently, when users log in or register, they will receive an ID token containing information about their roles and other details.

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1646850084,
  "nbf": 1646846484,
  "ver": "1.0",
  "iss": "https://azureb2cmydemo.b2clogin.com/d227dc46-1f9c-4a46-97a2-d3b83ed89a3a/v2.0/",
  "sub": "b5bb6799-77f9-43a6-b6bf-15eaff761600",
  "aud": "d1400d7d-a389-4c38-a36f-327e8e949017",
  "nonce": "defaultNonce",
  "iat": 1646846484,
  "auth_time": 1646846484,
  "oid": "b5bb6799-77f9-43a6-b6bf-15eaff761600",
  "name": "Test user",
  "country": "US",
  "given_name": "John",
  "family_name": "Smith",
  "extension_Roles": "Administrator, Developer",
  "emails": [
    "useremail@gmail.com"
  ],
  "tfp": "B2C_1_SignUpAndSignIn1"
}.[Signature]
Enter fullscreen mode Exit fullscreen mode

The final step for setting up an Azure B2C client involves integrating it with a secure API. Follow the setup process outlined in step 2 of the guide. Consequently, our web application will gain access to a secure API, which will be utilized to obtain Access and Refresh tokens.

Image description

Integration with Sitecore

According to the official documentation, federated authentication requires specific configuration settings for Sitecore. Additionally, considering our project's requirements, we needed to establish a password reset pipeline and implement the retrieval of Access and Refresh tokens. The following aspects needed implementation:

  • Identity Provider configuration
  • Obtaining Access and Refresh tokens
  • Mapping claims and user profile properties
  • Creating a virtual user
  • Generating URLs for Azure B2C access
  • Implementing an extra pipeline for password reset

Moreover, we need to address cookie management, the logout process, and session termination.

During the Sitecore setup and integration with Azure AD B2C, we reached significant conclusions:

  • All interactions with Azure AD B2C should occur through the Identity Provider, and link generation must be handled via the Sitecore pipeline. Even if direct requests seem feasible, we utilize the Sitecore Identity Server, which generates the State object and ensures internal processes function correctly.
  • To support multiple user flows concurrently, it's necessary to establish a separate provider for each user flow or policy. This insight emerged after discussions with Sitecore's official support.

Identity Provider Configuration

For Identity Provider configuration, the following steps are required:

  • Create a pipeline inheriting from IdentityProvidersProcessor
  • Override the ProcessCore method
  • Override the IdentityProviderName property
  • Register the pipeline within the Sitecore configuration

Below is the code for the class implementing the pipeline for user registration and login:

namespace SitecoreAzureB2CDemo.Pipelines
{
   public class SignUpAndSignInPipeline : IdentityProvidersProcessor
   {
       private readonly string _tenant = "azureb2cmydemo.onmicrosoft.com";
       private readonly string _aadInstance;
       private readonly string _metaAddress;
       private readonly string _redirectUri = "https://sitecoreazureb2cdemosc.dev.local/azureb2c";
       private readonly string _postLogoutRedirectUri = "https://sitecoreazureb2cdemosc.dev.local/azureb2c";
       private readonly string _clientId = "xxx"/>;
       private readonly string _clientSecret = "xxx"/>;
       private readonly string _scope = "https://azureb2cmydemo.onmicrosoft.com/tasks-api/tasks.read";
       protected override string IdentityProviderName => IdentityProviderNames.SignUpAndSignIn;
       protected virtual string Policy => "B2C_1_SignUpAndSignIn1";
       private readonly HttpClient _client = new HttpClient();

       private IdentityProvider IdentityProvider => GetIdentityProvider();

       public SignUpAndSignInPipeline(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration,
           ICookieManager cookieManager, BaseSettings settings) : base(federatedAuthenticationConfiguration,
           cookieManager, settings)
       {
           var aadInstanceTemplate = "https://azureb2cmydemo.b2clogin.com/{0}/{1}";
           _aadInstance = string.Format(aadInstanceTemplate, _tenant, Policy);
           _metaAddress = $"{_aadInstance}/v2.0/.well-known/openid-configuration";
       }

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

           var authenticationType = GetAuthenticationType();
           var options = new OpenIdConnectAuthenticationOptions(authenticationType)
           {
               Caption = IdentityProvider.Caption,
               AuthenticationMode = AuthenticationMode.Passive,
               RedirectUri = _redirectUri,
               PostLogoutRedirectUri = _postLogoutRedirectUri,
               ClientId = _clientId,
               Authority = _aadInstance,
               MetadataAddress = _metaAddress,
               UseTokenLifetime = true,
               TokenValidationParameters = new TokenValidationParameters { NameClaimType = Claims.Name },
               CookieManager = CookieManager,
               Notifications = new OpenIdConnectAuthenticationNotifications
               {
                   RedirectToIdentityProvider = OnRedirectToIdentityProvider,
                   SecurityTokenValidated = OnSecurityTokenValidated,
               }
           };

           args.App.UseOpenIdConnectAuthentication(options);
       }

       private async Task OnSecurityTokenValidated(
           SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> arg)
       {
           var identity = arg.AuthenticationTicket.Identity;

           identity.AddClaim(new Claim(Claims.IdToken, arg.ProtocolMessage.IdToken));

           //apply Sitecore claims tranformations
           arg.AuthenticationTicket.Identity.ApplyClaimsTransformations(
               new TransformationContext(FederatedAuthenticationConfiguration, IdentityProvider));
           arg.AuthenticationTicket = new AuthenticationTicket(identity, arg.AuthenticationTicket.Properties);
       }

       private Task OnRedirectToIdentityProvider(
           RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> arg)
       {
           var owinContext = arg.OwinContext;
           var protocolMessage = arg.ProtocolMessage;

           if (protocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
           {
               protocolMessage.Prompt = "login";
           }
           else if (protocolMessage.RequestType == OpenIdConnectRequestType.Logout)
           {
               protocolMessage.PostLogoutRedirectUri = BuildPostLogoutRedirectUri(owinContext);
           }

           return Task.CompletedTask;
       }
  }
}
Enter fullscreen mode Exit fullscreen mode

To incorporate this pipeline into the Sitecore configuration, follow these steps:

  • Create a new identityProvider section under /sitecore/federatedAuthentication/identityProviders
  • Establish a new section mapEntry under configuration/sitecore/federatedAuthentication/identityProvidersPerSites.

The resulting Sitecore configuration patch file should resemble this:

Image description

Place the Azure B2C configuration settings within the appSettings section of the Web.config file:

<appSettings configBuilders="SitecoreAppSettingsBuilder">
   <add key="AzureB2C.Tenant" value="azureb2cmydemo.onmicrosoft.com"/>
   <add key="AzureB2C.SignUpAndSignInPolicy" value="B2C_1_SignUpAndSignIn1"/>
   <add key="AzureB2C.PasswordResetPolicy" value="B2C_1_PasswordReset1"/>
   <add key="AzureB2C.ProfileEditingPolicy" value="B2C_1_ProfileEditing1"/>
   <add key="AzureB2C.ClientId" value="xxx"/>
   <add key="AzureB2C.ClientSecret" value="xxx"/>
   <add key="AzureB2C.RedirectUri" value="https://sitecoreazureb2cdemosc.dev.local/azureb2c"/>
   <add key="AzureB2C.PostLogoutRedirectUri" value="https://sitecoreazureb2cdemosc.dev.local/azureb2c"/>
   <add key="AzureB2C.AzureADInstance" value="https://azureb2cmydemo.b2clogin.com/{0}/{1}"/>
   <add key="AzureB2C.AccessTokenUri" value="https://azureb2cmydemo.b2clogin.com/azureb2cmydemo.onmicrosoft.com/{0}/oauth2/v2.0/token"/>
   <add key="AzureB2C.Scope" value="https://azureb2cmydemo.onmicrosoft.com/tasks-api/tasks.read" />
 </appSettings>
Enter fullscreen mode Exit fullscreen mode

Additionally, the following points deserve attention:

  • OnRedirectToIdentityProvider: Generate a PostLogoutRedirectUri reference to prevent endless redirects at session termination.
  • OnSecurityTokenValidated: Add supplementary information to claims, such as an ID token. Later, Access and Refresh tokens will also be added to claims.
  • ApplyClaimsTransformations method: This step is crucial, even if no claims transformations are defined in the configuration. Skipping it could lead to an "Ids claim is missing" exception.
  • Obtain the CookieManager via DI and explicitly pass it to OpenIdConnectAuthenticationOptions. This helps solve cookie-related issues, including the problem of excessive cookies causing the maximum request size to be exceeded, resulting in errors and incorrect handling of SameSite cookies. Refer to a hotfix with a detailed solution to this problem.

Obtaining Access and Refresh Tokens

To retrieve Access and Refresh tokens, the information provided to the user within the personal account is fetched from the secure API on the client side. These tokens are required to communicate with the API securely.

We achieve the acquisition of tokens for accessing the secure API within the event handler that handles the successful validation of the security token received from Azure B2C (OnSecurityTokenValidated). Subsequently, the received tokens need to be stored in claims, making them accessible throughout the project.

For the sake of clarity, we opted for a conventional API request without relying on third-party libraries. In a genuine project scenario, it's recommended to create a dedicated service to track token lifetimes and manage updates as necessary. Alternatively, you can utilize a third-party library that facilitates token validation and updates.

A comprehensive guide detailing the process of obtaining tokens can be found in the "Requesting an Access Token in Azure Active Directory B2C" section of the official Microsoft documentation.

private async Task OnSecurityTokenValidated(
   SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> arg)
{
   var identity = arg.AuthenticationTicket.Identity;

   var result = await GetToken(arg.ProtocolMessage.Code);

   identity.AddClaim(new Claim(Claims.IdToken, arg.ProtocolMessage.IdToken));
   identity.AddClaim(new Claim(Claims.AccessToken, result.AccessToken));
   identity.AddClaim(new Claim(Claims.RefreshToken, result.RefreshToken));

   //apply Sitecore claims tranformations
   arg.AuthenticationTicket.Identity.ApplyClaimsTransformations(
       new TransformationContext(FederatedAuthenticationConfiguration, IdentityProvider));
   arg.AuthenticationTicket = new AuthenticationTicket(identity, arg.AuthenticationTicket.Properties);
}

private async Task GetToken(string code)
{
   var getTokenUrl = string.Format(AzureB2CConfiguration.AccessTokenUri, Policy);

   var dict = new Dictionary<string, string>
   {
       { "grant_type", "authorization_code" },
       { "client_id", _clientId },
       { "client_secret", _clientSecret },
       { "scope",  $"{_scope}  offline_access" },
       { "code",  code },
       { "redirect_uri",  AzureB2CConfiguration.RedirectUri},
   };
   var requestBody = new FormUrlEncodedContent(dict);
   var response = await _client.PostAsync(getTokenUrl, requestBody);
   response.EnsureSuccessStatusCode();           

   var  responseBody = await response.Content.ReadAsStringAsync();
   var responseDto = JsonConvert.DeserializeObject(responseBody);
   return responseDto;
}
Enter fullscreen mode Exit fullscreen mode

Converting Claims and User Profile Properties

Depending on various work scenarios, there may arise a need to transform (or map) certain claims into others or into Sitecore user properties. Sitecore provides a built-in mechanism to facilitate these transformations. For more detailed insights, refer to the "Configure Federated Authentication" section in the official documentation.

Transformations can be configured in two ways:

  • At the level of a specific provider: within the identityProvidertransformations section – this will be applied solely to a single identity provider.
  • At the level of identityProviderssharedTransformations section – this will be applied to all identity providers registered in the application configuration.

Consequently, you can define shared transformations applicable to all providers within a single sharedTransformations section, and specific transformations can be established for individual providers as needed.

Let's examine a few examples:

Mapping the "Emails" Claim to "Email": The "Emails" claim is understood by Sitecore and is automatically mapped to the corresponding user property. To map it to "Email," include a transformation within identityProviderssharedTransformations.

<sharedTransformations>
   <transformation type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
       <sources hint="raw:AddSource">
           <claim name="emails" />
       </sources>
       <targets hint="raw:AddTarget">
           <claim name="email" />
       </targets>
       <keepSource>false</keepSource>
   </transformation>
</sharedTransformations>
Enter fullscreen mode Exit fullscreen mode

Mapping the "Roles" Claim to Individual User Roles: This requires a more intricate mapping process. Mapping claims to roles enables the system to authorize users based on their roles and manage access rights. We also included this mapping in sharedTransformations.

<sharedTransformations>
   <transformation name="map roles from Sitecore"
                   type="SitecoreAzureB2CDemo.Transformations.ClaimsToRolesTransformation, SitecoreAzureB2CDemo"
                   patch:after="transformation[@type='Sitecore.Owin.Authentication.IdentityServer.Transformations.ApplyAdditionalClaims, Sitecore.Owin.Authentication.IdentityServer']"
                   resolve="true" />
</sharedTransformations>
Enter fullscreen mode Exit fullscreen mode

To accomplish this, we developed a class inheriting from the Transformation class and implemented the required logic within it.

public class ClaimsToRolesTransformation : Transformation
{
   public override void Transform(ClaimsIdentity identity, TransformationContext context)
   {
       var claimValue = identity.Claims.GetClaimValue(Claims.Permissions);

       if (string.IsNullOrEmpty(claimValue))
       {
           return;
       }

       var userPermissions = claimValue.Split(',');
       foreach (var userPermission in userPermissions)
       {
           identity.AddClaim(new Claim(Claims.Role, userPermission));
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

Additionally, here's an example of mapping claims to profile properties stored within the user's profile.

<propertyInitializer
   type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication">
   <maps hint="list">
       <map name="emailClaim"
            type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication"
            resolve="true">
           <data hint="raw:AddData">
               <source name="email" />
               <target name="Email" />
           </data>
       </map>
</propertyInitializer>
Enter fullscreen mode Exit fullscreen mode

Creating a Virtual User

When dealing with federated authentication, a decision needs to be made whether to utilize a virtual user for the session's duration or to establish a persistent user. Since user management in our system is entirely delegated to the client's system, we opted for the virtual user option.

Initially, let's create a class that inherits from DefaultExternalUserBuilder. Generating a username is one of its primary tasks.

public class ExternalDomainUserBuilder : DefaultExternalUserBuilder
   {
       public ExternalDomainUserBuilder(ApplicationUserFactory applicationUserFactory, IHashEncryption hashEncryption)
           : base(applicationUserFactory, hashEncryption)
       {
       }

       public override ApplicationUser BuildUser(UserManager userManager,
           ExternalLoginInfo externalLoginInfo)
       {
           var appUser = base.BuildUser(userManager, externalLoginInfo);

           appUser.InnerUser.Profile.Save();

           return appUser;
       }

       private static void MapClaimToCustomProperty(ExternalLoginInfo source, ApplicationUser target, string claim, string propertyName)
       {
           var property = source.GetClaimValue(claim);

           if (!string.IsNullOrEmpty(property))
           {
               target.InnerUser.Profile.SetCustomProperty(propertyName, property);
           }
       }

       protected override string CreateUniqueUserName(UserManager userManager,
           ExternalLoginInfo externalLoginInfo)
       {
           if (userManager == null)
           {
               throw new ArgumentNullException(nameof(userManager));
           }

           if (externalLoginInfo == null)
           {
               throw new ArgumentNullException(nameof(externalLoginInfo));
           }

           var identityProvider =
               this.FederatedAuthenticationConfiguration.GetIdentityProvider(externalLoginInfo.ExternalIdentity);

           if (identityProvider == null)
           {
               throw new InvalidOperationException("Unable to retrieve identity provider for given identity");
           }

           var domain = identityProvider.Domain;

           var name = externalLoginInfo.GetClaimValue(Claims.Email);

           if (string.IsNullOrEmpty(name))
           {
               return GetDomainUserName(domain, externalLoginInfo.DefaultUserName);
           }

           return GetDomainUserName(domain, name.Replace(",", ""));
       }

       private string GetDomainUserName(string domain, string userName)
       {
           Sitecore.Diagnostics.Log.Info("Azure login user " + userName, this);
           return $"{domain}\\{userName}";
       }
   }


Enter fullscreen mode Exit fullscreen mode

Subsequently, we must register this class within the Sitecore configuration. To achieve this, introduce an externalUserBuilder section within federatedAuthenticationidentityProvidersPerSitesmapEntry. In this context, the IsPersistentUser property for virtual users should be set to false.

<externalUserBuilder
   type="SitecoreAzureB2CDemo.Helpers.ExternalDomainUserBuilder, SitecoreAzureB2CDemo" resolve="true">
   <IsPersistentUser>false</IsPersistentUser>
</externalUserBuilder>
Enter fullscreen mode Exit fullscreen mode

URL Generation for Azure B2C Access

Upon configuring one or more external identity providers, links to access them can be generated by invoking the Sitecore GetSignInUrlInfo pipeline. This pipeline will yield a collection of login links, one for each identity provider. The desired link can be selected based on the provider's name.

private string GetSignInUrl(string identityProviderName, string returnUrl)
{
   if (string.IsNullOrEmpty(identityProviderName))
   {
       throw new ArgumentNullException(nameof(identityProviderName));
   }

   var pipelineManager = (BaseCorePipelineManager)ServiceLocator.ServiceProvider.GetService(typeof(BaseCorePipelineManager));
   var args = new Sitecore.Pipelines.GetSignInUrlInfo.GetSignInUrlInfoArgs(Sitecore.Context.Site.Name, returnUrl);
   Sitecore.Pipelines.GetSignInUrlInfo.GetSignInUrlInfoPipeline.Run(pipelineManager, args);

   var url = args.Result.FirstOrDefault(x => x.IdentityProvider == identityProviderName);

   return url?.Href;
}
Enter fullscreen mode Exit fullscreen mode

It's important to note that the generated link does not direct to Azure AD B2C directly but instead leads to the Sitecore Identity Server:

/identity/externallogin?authenticationType=SignUpAndSignIn&ReturnUrl=%2fidentity%2fexternallogincallback%3fReturnUrl%3d%252fazureb2c%26sc_site%3dwebsite%26authenticationSource%3dDefault&sc_site=website

To engage with the identity server, a POST request needs to be dispatched. An XHR request isn't suitable in this context as a 302 redirect to Azure AD B2C is expected in response.

This is where the auto-submit form proves beneficial:

<html>
<body onload='sessionStorage.clear(); document.forms[""form""].submit()'>
<form name='form' action='@Model.SignInUrl' method='post'>
   <input type="submit" value="Login">
</form>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Examining the code above reveals that the form contains a JavaScript snippet that executes automatically once the HTML loading is completed.

Subsequently, the identity server redirects us to Azure AD B2C, incorporating the necessary parameters for internal processing.

https://azureb2cmydemo.b2clogin.com/azureb2cmydemo.onmicrosoft.com/b2c_1_signupandsignin1/oauth2/v2.0/authorize?client_id=d1400d7d-a389-4c38-a36f-327e8e949017&redirect_uri=https%3A%2F%2Fsitecoreazureb2cdemosc.dev.local%2Fazureb2c&response_type=code%20id_token&scope=openid%20profile&state=OpenIdConnect.AuthenticationProperties%3Dhwe7m0qKgXQKiMza5ViYcrjAehjrhE-vO2CklGHCDJ4_N2iEbt5leLP0oRG2Y1LujmtZsgHeZ1zzoIHebutJdAUxXr0ZZ9BuQaSLycs-2Eb_nsN2BkVV_qDDJHtJJZsBqbc6v6R1cAeC-WLBJr84nRGrqhTt1BGbAwNPPPv4JfHkUl8d9PjQZf_oXMZHPnGfSLt0J0h1bYYkvpM2k6hk725wnByHDQmWYzlnaCzdzaV_iPisnPuennulGPllC5Vd2OQ4JogZ6M_A_I2uWj5y351rOxv0tmqiRbQpOnUkfAX1JUtnAFxoYAqlS2ij84TeMstnD3MFGpTmAppwFgatiw&response_mode=form_post&nonce=637825709211329023.ZDA2Y2E4MTgtNjdkMi00YTFmLWEyNDYtMzU1OWUyMzMzYzkzODNjODcyZWQtMWFiOS00MmE3LTk3N2EtNGU0NzhhMmE1OGI0&prompt=login&x-client-SKU=ID_NET461&x-client-ver=5.3.0.0

Additional Pipeline for Password Reset

Recalling the recommendation from Sitecore's technical support, separate Identity Provider classes are advised for each Azure B2C flow/policy. Consequently, an additional Identity Provider is required for password reset purposes.

To achieve this, a new pipeline that inherits from SignUpAndSignInPipeline is created:

public class PasswordResetPipeline : SignUpAndSignInPipeline
{
   public PasswordResetPipeline(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration,
       ICookieManager cookieManager, BaseSettings settings) : base(federatedAuthenticationConfiguration,
       cookieManager, settings)
   {
   }

   protected override string IdentityProviderName => IdentityProviderNames.PasswordReset;
   protected override string Policy => "B2C_1_PasswordReset1";
}
Enter fullscreen mode Exit fullscreen mode

Subsequently, this pipeline needs to be registered in the configuration, mirroring the steps outlined earlier in the "Identity Provider Configuration" section for SignUpAndSignInPipeline.

<pipelines>
   <owin.identityProviders>
       <processor type="SitecoreAzureB2CDemo.Pipelines.SignUpAndSignInPipeline, SitecoreAzureB2CDemo" resolve="true" />
       <processor type="SitecoreAzureB2CDemo.Pipelines.PasswordResetPipeline, SitecoreAzureB2CDemo" resolve="true" />
   </owin.identityProviders>
</pipelines>

<federatedAuthentication
   type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">

   <identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
       <mapEntry name="Azure AD B2C for website"
                 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='SignUpAndSignIn']" />
               <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='PasswordReset']" />
           </identityProviders>
           <externalUserBuilder
               type="SitecoreAzureB2CDemo.Helpers.ExternalDomainUserBuilder, SitecoreAzureB2CDemo" resolve="true">
               <IsPersistentUser>false</IsPersistentUser>
           </externalUserBuilder>
       </mapEntry>
   </identityProvidersPerSites>

   <identityProviders hint="list:AddIdentityProvider">
       <identityProvider id="SignUpAndSignIn" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
           <param desc="name">SignUpAndSignIn</param>
           <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
           <caption>SignUpAndSignIn</caption>
           <domain>extranet</domain>
           <enabled>true</enabled>
           <triggerExternalSignOut>true</triggerExternalSignOut>
           <transformations hint="list:AddTransformation">
           </transformations>
       </identityProvider>
       <identityProvider id="PasswordReset" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
           <param desc="name">PasswordReset</param>
           <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
           <caption>PasswordReset</caption>
           <domain>extranet</domain>
           <enabled>true</enabled>
           <triggerExternalSignOut>true</triggerExternalSignOut>
           <transformations hint="list:AddTransformation">
           </transformations>
       </identityProvider>
   </identityProviders>

Enter fullscreen mode Exit fullscreen mode

This approach enables us to work with multiple custom policies. When invoking the GetSignInUrlInfoPipeline, a collection of links is obtained, and the desired Identity Provider can be selected based on its name.

private string GetSignInUrl(string identityProviderName, string returnUrl)
        {
            if (string.IsNullOrEmpty(identityProviderName))
            {
                throw new ArgumentNullException(nameof(identityProviderName));
            }

            var pipelineManager = (BaseCorePipelineManager)ServiceLocator.ServiceProvider.GetService(typeof(BaseCorePipelineManager));
            var args = new Sitecore.Pipelines.GetSignInUrlInfo.GetSignInUrlInfoArgs(Sitecore.Context.Site.Name, returnUrl);
            Sitecore.Pipelines.GetSignInUrlInfo.GetSignInUrlInfoPipeline.Run(pipelineManager, args);

            var url = args.Result.FirstOrDefault(x => x.IdentityProvider == identityProviderName);

            return url?.Href;
        }
Enter fullscreen mode Exit fullscreen mode

What We Achieved

Let's take a look at the results of our efforts. We began by attempting to log in. Initially, we navigated to the test page where we observed that "Is Authenticated" was set to false. This indicates that we didn't have an authenticated user yet; no claims were present, and the user profile properties remained unfilled.

Image description

Upon clicking the "Login" button, the browser directed us to the Azure AD B2C login page.

Image description

After entering our login credentials and clicking the "Sign in" button, we were returned to our application's test page. As depicted in the image below, the authentication process was successful. The user was authorized, and we could see the completed profile properties, claims, and assigned roles.

Image description

To ensure the effectiveness of the password reset functionality, we clicked on the "Password reset" button, which led us to the password reset page.

Image description

Image description

Conclusion

And there you have it! We've accomplished all the necessary tasks, and everything is functioning as expected. We have seamlessly implemented the login and logout processes for personal accounts on Sitecore via Azure AD B2C. In addition, we've gained a solid understanding of how to manage various user policies. We've empowered users with the capability to reset and modify their passwords, all while ensuring meticulous cookie management.

We'd like to emphasize the invaluable assistance provided by Sitecore's official technical support. They played a significant role in guiding us toward solutions for specific challenges we encountered during this task. Their support was instrumental in resolving issues such as the endless redirect during logout and the utilization of separate identity providers for distinct user policies. We are grateful for their readiness to aid developers in overcoming obstacles and ensuring successful project implementation!

Author of the article: Dmitry Katasonov

Top comments (0)