8

I have setup federated authentication for my Sitecore 9.1 website, to allow end users 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 end users, but when I try to login to Sitecore, I receive 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 don't want to use my custom provider for Sitecore client login, but only want to use it for end user 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 identity provider for Sitecore client logins?

6
  • 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. May 6, 2019 at 15:24
  • Right now I'm just trying to get it to work locally (combined cd-cm) :)
    – Hos
    May 6, 2019 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? May 9, 2019 at 10:37
  • @ArvindGehlot while clicking on default "Log in" button...which is why I am confused now
    – Hos
    May 9, 2019 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. May 9, 2019 at 13:43

2 Answers 2

9

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!

17
  • What do you have for the Feature.AzureAdB2C.Models.AuthenticationConfigurator class ? Jun 6, 2019 at 17:47
  • 1
    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 ? Jun 6, 2019 at 17:48
  • AuthenticationConfigurator is just used for serviceregistration, i.e. an implementation of IServicesConfigurator (used for Dependency Injection)
    – Hos
    Jun 6, 2019 at 18:27
  • 1
    Thanks.. was able to get the integration working.. Jun 7, 2019 at 0:09
  • @Hos am currently working an Azure B2C Login for end users , what does the route configurations do ? why is it required ? Jul 24, 2019 at 11:48
1

The other answer wasn't clear on exactly the problem, I came across this in an Auth0 B2C integration and removing these from the IdentityProviderProcessor fixed it. I think these may already be getting registered by Sitecore.

    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.
    });
5
  • It seems, CookieAuthentication is added as part of this Owin pipeline: <processor name="CookieAuthentication" type="Sitecore.Owin.Authentication.Pipelines.Initialize.CookieAuthentication, Sitecore.Owin.Authentication" /> and SeDetaultSigninAsAuthenticationType is set in this pipeline: <processor type="Sitecore.Owin.Authentication.Pipelines.Initialize.ExternalCookieAuthentication, Sitecore.Owin.Authentication" />
    – Hos
    Aug 28, 2020 at 4:49
  • args.App.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); This piece of code was the main culprit for me
    – NewbieHere
    Oct 6, 2021 at 12:55
  • Can you please provide your Login Action method as well?
    – NewbieHere
    Oct 8, 2021 at 6:31
  • 1
    @NewbieHere I am sorry, but I no longer have access to the source code. It was written at a previous job
    – Hos
    Oct 13, 2021 at 8:11
  • @Hos Thank you for your reply.
    – NewbieHere
    Oct 13, 2021 at 10:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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