4

I have setup federated authentication for my Sitecore 9.1 website, to allow endusers to login using Azure B2C AD. Our website is following Helix architecture.

This is my code:

Portal.Website.config:

<federatedAuthentication>
  <identityProvidersPerSites>
    <mapEntry name="portal" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication">
      <sites hint="list">
        <site>portal</site>
      </sites>
      <identityProviders hint="list:AddIdentityProvider">
        <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='AzureAdB2C']"/>
      </identityProviders>
      <externalUserBuilder type="Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, Sitecore.Owin.Authentication">
        <param desc="isPersistentUser">false</param>
      </externalUserBuilder>
    </mapEntry>
  </identityProvidersPerSites>
</federatedAuthentication>

Configuration for Helix module that implements Azure B2C logic (Feature.AzureB2C.config):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="Feature.AzureAdB2C.Models.Routes.RegisterAuthRoute, Feature.AzureAdB2C"
                   patch:before="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']" resolve="true" />
      </initialize>
      <owin.identityProviders>
        <!--This is the custom processor that gets executed when azure AD posts the token to Sitecore-->
        <processor type="Feature.AzureAdB2C.AzureAdB2CIdentityProviderProcessor, Feature.AzureAdB2C" resolve="true" />
      </owin.identityProviders>
    </pipelines>
    <federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">
      <!--Provider mappings to sites-->
      <identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
      </identityProvidersPerSites>

      <!--Definitions of providers-->
      <identityProviders hint="list:AddIdentityProvider">
        <!--Facebook provider-->
        <identityProvider id="AzureAdB2C" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
          <param desc="name">AzureAdB2C</param>
          <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
          <caption>AzureAdB2C</caption>
          <domain>portal</domain>
          <transformations hint="list:AddTransformation">
            <transformation name="Name Identifier Claim" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="http://schemas.microsoft.com/identity/claims/objectidentifier" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" />
              </targets>
              <keepSource>true</keepSource>
            </transformation>
          </transformations>
        </identityProvider>
      </identityProviders>

      <!--List of all shared transformations-->
      <sharedTransformations>
      </sharedTransformations>

      <!--Property mappings initializer-->
      <propertyInitializer type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication">
        <!--List of property mappings
                    Note that all mappings from the list will be applied to each providers-->
        <maps hint="list">
          <!--The mapping sets the Email property of the user profile from emailaddress claim-->
          <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>
        </maps>
      </propertyInitializer>

    </federatedAuthentication>
  </sitecore>
</configuration>

Route registration:

public virtual void Process(PipelineArgs args)
{
    RouteTable.Routes.MapRoute("AuthRoute", "auth/sso/{action}", new
    {
        controller = "Sso"
    });

    SsoLogger.Info("RegisterAuthRoute - Registered route: auth/sso/{action}");

    RouteTable.Routes.MapRoute("LoginContentRoute", "api/content/{action}", new
    {
        controller = "Content"
    });

    SsoLogger.Info("LoginContentRoute - Registered route: api/login/{action}");
}

IdentityProviderProcessor implementation:

public class AzureAdB2CIdentityProviderProcessor : IdentityProvidersProcessor
{
    private readonly ISsoConfigurationRepository _ssoConfigurationRepository;
    private readonly string _aadInstance = SsoConfiguration.AadInstance;
    private readonly string _tenant = SsoConfiguration.Tenant;
    private readonly string _ssoAuthenticationRootPath = SsoConfiguration.Routes.RootPath;

    protected override string IdentityProviderName => "AzureAdB2C";

    public AzureAdB2CIdentityProviderProcessor(
        FederatedAuthenticationConfiguration federatedAuthenticationConfiguration, ICookieManager cookieManager,
        BaseSettings settings) : base(federatedAuthenticationConfiguration, cookieManager, settings)
    {
        _ssoConfigurationRepository = ServiceLocator.ServiceProvider.GetService<ISsoConfigurationRepository>();
    }

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

        var identityProvider = GetIdentityProvider();
        var authenticationType = GetAuthenticationType();

        args.App.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
        args.App.UseCookieAuthentication(new CookieAuthenticationOptions()
        {
            CookieName = SsoConfiguration.SsoCookieName,
            CookiePath = _ssoAuthenticationRootPath, // Scope cookies to the authentication root path in order to avoid having the cookie included in every normal request.
            TicketDataFormat = new TicketDataFormat(new MachineKeyProtector()) // In order to be able to decrypt the authentication ticket stored in the cookie later on, we use protection based on the machine key.
        });
        List<B2CConfig> ssoSettings = _ssoConfigurationRepository.GetAllSettings();

        foreach (var config in ssoSettings)
        {
            args.App.UseOpenIdConnectAuthentication(CreateOptionsFromSiteConfig(config));
        }
    }

    private OpenIdConnectAuthenticationOptions CreateOptionsFromSiteConfig(B2CConfig config)
    {
        OpenIdConnectAuthenticationOptions options = new OpenIdConnectAuthenticationOptions();
        var identityProvider = GetIdentityProvider();

        // For each policy, give OWIN the policy-specific metadata address, and
        // set the authentication type to the id of the policy
        options.MetadataAddress = string.Format(_aadInstance, _tenant, config.Policy);
        options.AuthenticationType = config.Policy;

        options.RedirectUri = config.AzureReplyUri;
        options.PostLogoutRedirectUri = config.RedirectAfterLogoutUri;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "emails"
        };

        options.Notifications = new OpenIdConnectAuthenticationNotifications()
        {
            AuthenticationFailed = AuthenticationFailed,
            RedirectToIdentityProvider = notification =>
            {
                return Task.FromResult(notification.ProtocolMessage.UiLocales = config.UiLocale ?? string.Empty);
            },
            SecurityTokenValidated = notification =>
            {
                notification.AuthenticationTicket.Identity.AddClaim(new Claim("idp", "azureadb2c"));

                // transform all claims
                ClaimsIdentity identity = notification.AuthenticationTicket.Identity;
                foreach (Transformation current in identityProvider.Transformations)
                {
                    current.Transform(identity, new TransformationContext(FederatedAuthenticationConfiguration, identityProvider));
                }

                return Task.FromResult(0);
            }
        };

        // These are standard OpenID Connect parameters, with values pulled from web.config
        options.ClientId = config.ClientId;
        //http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
        options.Scope = "openid";
        //http://openid.net/specs/openid-connect-core-1_0.html#Authentication
        options.ResponseType = "id_token";

        return options;
    }
}

Example of B2CConfig class:

public class B2CConfig
{
    public string Policy { get; }

    public string AzureReplyUri { get; }

    public string LogoutRedirectUri { get; }

    public string ClientId { get; }

    public string UiLocale { get; }    
}

The login works for endusers, but when I try to login to Sitecore, I receieve the error message: "Error: "Unsuccessful login with external provider".

As far as I can understand from blogs and https://kb.sitecore.net/articles/252884 it is due to the fact that the "name"-claim is missing.

However, since I dont want to use my custom provder for Sitecore client login, but only want to use it for enduser login on the website, I am confused as to why Sitecore throws this error? Is there somewhere I have to explicitly tell Sitecore NOT to use my custom identityprovider for Sitecore client logins?

  • have you configured the federated auth in CM servers as well.? if you don't want the custom provider to be used for client login then i guess you can ignore this integration in CM servers. – Gobinath Saminathan May 6 at 15:24
  • Right now I'm just trying to get it to work locally (combined cd-cm) :) – Hos May 6 at 15:54
  • Hi @Hos, you are getting "Unsuccessful login with external provider" error message while clicking on default "Log In" button or custom "AzureAdB2C" external authentication button? – Arvind Gehlot May 9 at 10:37
  • @ArvindGehlot while clicking on default "Log in" button...which is why I am confused now – Hos May 9 at 11:41
  • @Hos I guess, there is something in your routeing table as you register a processor Feature.AzureAdB2C.Models.Routes.RegisterAuthRoute in initialize pipeline. Please re-verify what route you have registered and whether it is impacting your default login request or not. – Arvind Gehlot May 9 at 13:43
6

I finally managed to get both Sitecore login (standard) and enduser login (Azure B2C AD) to work at the same time. I did a few changes and I am not 100% sure exactly which of them made the difference, so I will just list them and post the working code.

Changes:

  • Moved the Feature.AzureB2C.config (config file for Identity provider) out of a "zFeature" folder and into a "Feature" folder
  • Added service registrations for BaseAuthenticationManager, BaseTicketManager and BasePreviewManager to my config file (Feature.AzureAdB2C.config)
  • Simplified IdentityProviderProcessor implementation (see below)
  • Set authenticationmode in IdentityProviderProcessor

    public class AzureAdB2CIdentityProviderProcessor : IdentityProvidersProcessor
    {
    
        private readonly ISsoConfigurationRepository _ssoConfigurationRepository;
        private readonly string _aadInstance = SsoConfiguration.AadInstance;
        private readonly string _tenant = SsoConfiguration.Tenant;
        private readonly string _ssoAuthenticationRootPath = SsoConfiguration.Routes.RootPath;
    
        protected override string IdentityProviderName => "AzureAdB2C";
    
        public AzureAdB2CIdentityProviderProcessor(
            FederatedAuthenticationConfiguration federatedAuthenticationConfiguration, ICookieManager cookieManager,
            BaseSettings settings) : base(federatedAuthenticationConfiguration, cookieManager, settings)
        {
            _ssoConfigurationRepository = ServiceLocator.ServiceProvider.GetService<ISsoConfigurationRepository>();
        }
    
        protected override void ProcessCore(IdentityProvidersArgs args)
        {
            Assert.ArgumentNotNull(args, nameof(args));
    
            List<B2CConfig> ssoSettings = _ssoConfigurationRepository.GetAllSettings();
    
            foreach (var config in ssoSettings)
            {
                args.App.UseOpenIdConnectAuthentication(CreateOptionsFromSiteConfig(config));
            }
        }
    
        private OpenIdConnectAuthenticationOptions CreateOptionsFromSiteConfig(B2CConfig config)
        {
            OpenIdConnectAuthenticationOptions options = new OpenIdConnectAuthenticationOptions();
            var identityProvider = GetIdentityProvider();
    
            // For each policy, give OWIN the policy-specific metadata address, and
            // set the authentication type to the id of the policy
            options.MetadataAddress = string.Format(_aadInstance, _tenant, config.Policy);
            options.AuthenticationType = GetAuthenticationType();
            options.AuthenticationMode = AuthenticationMode.Passive;
            options.RedirectUri = config.AzureReplyUri;
            options.PostLogoutRedirectUri = config.RedirectAfterLogoutUri;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "emails"
            };
    
            options.Notifications = new OpenIdConnectAuthenticationNotifications()
            {
                AuthenticationFailed = AuthenticationFailed,
                RedirectToIdentityProvider = notification =>
                {
                    return Task.FromResult(notification.ProtocolMessage.UiLocales = config.UiLocale ?? string.Empty);
                },
                SecurityTokenValidated = notification =>
                {
                    notification.AuthenticationTicket.Identity.AddClaim(new Claim("idp", "azureadb2c"));
    
                    // transform all claims
                    ClaimsIdentity identity = notification.AuthenticationTicket.Identity;
                    notification.AuthenticationTicket.Identity.ApplyClaimsTransformations(new TransformationContext(FederatedAuthenticationConfiguration, identityProvider));
    
                    return Task.CompletedTask;
                }
            };
    
            // These are standard OpenID Connect parameters, with values pulled from web.config
            options.ClientId = config.ClientId;
            //http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
            options.Scope = "openid";
            //http://openid.net/specs/openid-connect-core-1_0.html#Authentication
            options.ResponseType = "id_token";
    
            return options;
        }
    }
    

Feature.AzureAdB2C.config:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="Feature.AzureAdB2C.Models.Routes.RegisterAuthRoute, Feature.AzureAdB2C"
                   patch:before="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']" resolve="true" />
      </initialize>
      <owin.identityProviders>
        <!--This is the custom processor that gets executed when azure AD posts the token to Sitecore-->
        <processor type="Feature.AzureAdB2C.AzureAdB2CIdentityProviderProcessor, Feature.AzureAdB2C" resolve="true" />
      </owin.identityProviders>
    </pipelines>
    <services>
      <configurator type= "Feature.AzureAdB2C.Models.AuthenticationConfigurator, Feature.AzureAdB2C"/>
      <register serviceType="Sitecore.Abstractions.BaseAuthenticationManager, Sitecore.Kernel"
                implementationType="Sitecore.Owin.Authentication.Security.AuthenticationManager, Sitecore.Owin.Authentication"
                lifetime="Singleton" />
      <register serviceType="Sitecore.Abstractions.BaseTicketManager, Sitecore.Kernel"
                implementationType="Sitecore.Owin.Authentication.Security.TicketManager, Sitecore.Owin.Authentication"
                lifetime="Singleton" />
      <register serviceType="Sitecore.Abstractions.BasePreviewManager, Sitecore.Kernel"
                implementationType="Sitecore.Owin.Authentication.Publishing.PreviewManager, Sitecore.Owin.Authentication"
                lifetime="Singleton" />
    </services>
    <federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">
      <!--Provider mappings to sites-->
      <identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
      </identityProvidersPerSites>

      <!--Definitions of providers-->
      <identityProviders hint="list:AddIdentityProvider">
        <identityProvider id="AzureAdB2C" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
          <param desc="name">AzureAdB2C</param>
          <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
          <caption>AzureAdB2C</caption>
          <domain>portal</domain>
          <transformations hint="list:AddTransformation">
            <transformation name="Name Identifier Claim" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="http://schemas.microsoft.com/identity/claims/objectidentifier" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" />
              </targets>
              <keepSource>true</keepSource>
            </transformation>
          </transformations>
        </identityProvider>
      </identityProviders>

      <!--List of all shared transformations-->
      <sharedTransformations>
      </sharedTransformations>

      <!--Property mappings initializer-->
      <propertyInitializer type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication">
      </propertyInitializer>

    </federatedAuthentication>
  </sitecore>
</configuration>

I hope it helps someone else!

  • What do you have for the Feature.AzureAdB2C.Models.AuthenticationConfigurator class ? – scFootsteps Jun 6 at 17:47
  • I'm getting following error when try to login "Error: Unsuccessful login with external provider." . Do you have any idea on the reason for that ? – scFootsteps Jun 6 at 17:48
  • AuthenticationConfigurator is just used for serviceregistration, i.e. an implementation of IServicesConfigurator (used for Dependency Injection) – Hos Jun 6 at 18:27
  • Thanks.. was able to get the integration working.. – scFootsteps Jun 7 at 0:09
  • @Hos am currently working an Azure B2C Login for end users , what does the route configurations do ? why is it required ? – SitecoreNoob Jul 24 at 11:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.