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?