Sitecore Website Federated Authentication with Azure AD B2C with OpenID Connect
Sitecore Identity, Federated Authentication and Federation Gateway
If you are already familiar with the differences between Sitecore Federated Authentication with Sitecore Identity VS Sitecore Identity as a Federation Gateway, please skip to the next section. Otherwise, it's essential to understand the differences as they are consistently being mixed up.
Sitecore uses OpenID Connect, so some of the terms are from OpenID Connect 1.0 and OAuth 2.0 - because OpenID Connect extends OAuth. I recommend having some reading if they are also new to you.
- To have Federated Authentication with Sitecore, we need to have an Identity Provider.
- Sitecore Identity Server is the out of the box Identity Provider that's set up with Sitecore shell site to provide Federated Authentication.
- There are two options when integrating a new Identity Provider
- Setup the new Identity Provider with Sitecore directly for Federated Authentication
- Setup the new Identity Provider with Sitecore Identity where Sitecore Identity act as a Federation Gateway. In this case, Sitecore still has Sitecore Identity Server as the Identity Provider.
What do those two options look like?
- External Identity provider directly setup with Sitecore for Federated Authentication:
- This option is more suitable for public websites which mean users come to Sitecore sites and are redirected to the external Identity Provider to login and then are redirected back to Sitecore sites.
- Sitecore client (shell) can keep on using Sitecore Identity Server. Both can stay behind DMZ if required.
- Sitecore Identity Server as the Federation Gateway to external Identity Providers:
- This option is more suitable for allowing Sitecore users (like authors) to log in to Sitecore client via external Identity providers.
- If this option is selected for websites, Sitecore Identity Server must be exposed to the Internet.
There are other differences, won't go into too much detail here. But hopefully, this gives you a good overview of Federated Authentication in the new Sitecore versions.
This post will be about option 1 - Sitecore Website Federated Authentication with Azure AD B2C. If you are interested in Option 2, which is set up Azure AD B2C with Sitecore Identity, Jason has created an excellent article about this already: Azure AD B2C with Sitecore Identity. He also provided a lot of help when I did this post 🙂
Sitecore Website Federated Authentication with Azure AD B2C
The Sitecore version used in this is 9.3.0. Here are the steps:
- Have an Azure AD B2C instance ready.
- Register a new App in Azure AD B2C. Collect the following information:
AD Tenant Name: Orange.onmicrosoft.com
Application (Client) ID: xxxxxx-fe0f-4c1a-8101-xxxxxxxx
- Create a User Flow Policy of Type "Sign up and sign in". Collect the following information
Custom User Flow Name: B2C_1_signupsignin
- You can test accessing the below URL to make sure your AD B2C OpenID Connect endpoint is up. This is where you can see all your possible claims too.
https://Orange.b2clogin.com/tfp/Orange.onmicrosoft.com/B2C_1_signupsignin/v2.0/.well-known/openid-configuration
- Please make sure the Sitecore instance has OWIN and Federated Authentication both enabled. Then there are three steps:
- Setup an Asp.Net project. Below are some main Nuget packages you will need.
...
<package id="Microsoft.IdentityModel.Protocols.OpenIdConnect" version="5.2.2" targetFramework="net472" />
<package id="Microsoft.IdentityModel.Tokens" version="5.2.2" targetFramework="net472" />
<package id="Microsoft.Owin" version="4.0.0" targetFramework="net472" />
<package id="Microsoft.Owin.Security" version="4.0.0" targetFramework="net472" />
<package id="Microsoft.Owin.Security.OpenIdConnect" version="4.0.0" targetFramework="net472" />
<package id="Owin" version="1.0" targetFramework="net472" />
<package id="Sitecore.Kernel" version="9.3.0" targetFramework="net472" />
<package id="Sitecore.Mvc" version="9.3.0" targetFramework="net472" />
<package id="Sitecore.Mvc.Analytics" version="9.3.0" targetFramework="net472" />
<package id="Sitecore.Owin.Authentication" version="9.3.0" targetFramework="net472" />
...
- Create a custom IdentityProvidersProcessor that inherits
Sitecore.Owin.Authentication.Pipelines.IdentityProviders.IdentityProvidersProcessor
- Below is a simple implementation that works
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using Sitecore.Abstractions;
using Sitecore.Diagnostics;
using Sitecore.Owin.Authentication.Configuration;
using Sitecore.Owin.Authentication.Extensions;
using Sitecore.Owin.Authentication.Pipelines.IdentityProviders;
using Sitecore.Owin.Authentication.Services;
using System.Threading.Tasks;
namespace AzureB2CSitecoreFederated.Pipelines
{
public class AzureB2C : IdentityProvidersProcessor
{
public AzureB2C(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration,
ICookieManager cookieManager,
BaseSettings settings)
: base(federatedAuthenticationConfiguration, cookieManager, settings)
{
}
protected override string IdentityProviderName => "AzureB2C";
protected override void ProcessCore(IdentityProvidersArgs args)
{
Assert.ArgumentNotNull(args, nameof(args));
var identityProvider = GetIdentityProvider();
var authenticationType = GetAuthenticationType();
string tenant = Settings.GetSetting("Sitecore.Feature.Accounts.AzureB2C.Tenant");
string signupsigninpolicy = Settings.GetSetting("Sitecore.Feature.Accounts.AzureB2C.Policy");
string clientId = Settings.GetSetting("Sitecore.Feature.Accounts.AzureB2C.ClientId");
string aadInstanceraw = Settings.GetSetting("Sitecore.Feature.Accounts.AzureB2C.AadInstance");
var aadInstance = string.Format(aadInstanceraw, tenant, signupsigninpolicy);
var metaAddress = $"{aadInstance}/v2.0/.well-known/openid-configuration";
var redirectUri = Settings.GetSetting("Sitecore.Feature.Accounts.AzureB2C.RedirectUri");
var options = new OpenIdConnectAuthenticationOptions(authenticationType)
{
Caption = identityProvider.Caption,
AuthenticationMode = AuthenticationMode.Passive,
RedirectUri = redirectUri,
ClientId = clientId,
Authority = aadInstance,
MetadataAddress = metaAddress,
UseTokenLifetime = true,
TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name" },
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = context =>
{
// Note 1 ------------------------- Please see after all steps
var debugClaims = context.AuthenticationTicket.Identity?.Claims;
context.AuthenticationTicket.Identity.ApplyClaimsTransformations(new TransformationContext(this.FederatedAuthenticationConfiguration, identityProvider));
return Task.CompletedTask;
}
}
};
args.App.UseOpenIdConnectAuthentication(options);
}
}
}
- Then create a config file like below. Note the collected information are populated in the settings
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
<sitecore role:require="Standalone or ContentDelivery or ContentManagement">
<settings>
<setting name="Sitecore.Feature.Accounts.AzureB2C.Tenant" value="Orange.onmicrosoft.com" />
<setting name="Sitecore.Feature.Accounts.AzureB2C.Policy" value="B2C_1_signupsignin" />
<setting name="Sitecore.Feature.Accounts.AzureB2C.ClientId" value="xxxxxx-fe0f-4c1a-8101-xxxxxxxx" />
<setting name="Sitecore.Feature.Accounts.AzureB2C.AadInstance" value="https://Orange.b2clogin.com/tfp/{0}/{1}" />
<setting name="Sitecore.Feature.Accounts.AzureB2C.RedirectUri" value="https://sitecorewebsite.com/" />
</settings>
<pipelines>
<owin.identityProviders>
<processor type="AzureB2CSitecoreFederated.Pipelines.AzureB2C, AzureB2CSitecoreFederated" 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='AzureB2C']" />
</identityProviders>
<externalUserBuilder type="Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, Sitecore.Owin.Authentication" resolve="true">
<!-- Note 2 ------------------------- Please see after all steps -->
<IsPersistentUser>false</IsPersistentUser>
</externalUserBuilder>
</mapEntry>
</identityProvidersPerSites>
<identityProviders hint="list:AddIdentityProvider">
<identityProvider id="AzureB2C" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
<param desc="name">AzureB2C</param>
<param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
<caption>AzureB2C</caption>
<domain>customerdomain</domain>
<enabled>true</enabled>
<transformations hint="list:AddTransformation">
<!-- Note 3 ------------------------- Please see after all steps -->
<transformation type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
<sources hint="raw:AddSource">
<claim name="jobTitle" value="SomeRoleText" />
</sources>
<targets hint="raw:AddTarget">
<claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="CustomDomain\SomeRole" />
</targets>
<keepSource>true</keepSource>
</transformation>
</transformations>
</identityProvider>
</identityProviders>
<sharedTransformations></sharedTransformations>
<!-- Note 4 ------------------------- Please see after all steps -->
<propertyInitializer type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication">
<maps hint="list">
<map name="email claim" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
<data hint="raw:AddData">
<source name="emails" />
<target name="Email" />
</data>
</map>
<map name="full_name" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
<data hint="raw:AddData">
<source name="name" />
<target name="FullName" />
</data>
</map>
<map name="first_name" 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="FirstName" />
</data>
</map>
<map name="last_name" 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/surname" />
<target name="LastName" />
</data>
</map>
<map name="streetAddress" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
<data hint="raw:AddData">
<source name="streetAddress" />
<target name="AddressProp" />
</data>
</map>
</maps>
</propertyInitializer>
</federatedAuthentication>
</sitecore>
</configuration>
Note that the integration is using the new b2clogin.com endpoints of Azure AD B2C, not http://login.microsoftonline.com/ since it will be deprecated by the end of 2020. More details here: https://docs.microsoft.com/en-us/azure/active-directory-b2c/b2clogin
Also please see the notes in the code and config files (For example, can search "Note 1" on the page to find its location in the demo code/configs)
- Note 1: This section of code is required so this custom Identity Provider Processor picks up the shared transforms that are set up out of the box by Sitecore. One of which is the "idp" claim. If you do not have this section, very likely you can get the error "idp claim is missing". Having sharedTransformations/setIdpClaim section does not have any effort on your custom identity provider if it doesn't even try to apply shared transformations.
- Note 2: You can choose to persist users or having virtual users. I had virtual users in this demo.
- Note 3: Azure AD B2C has a limitation that it doesn't pass group information in the claims. There are ways to customize the AD side to enable the claim however in this demo it just mapped to some claims and picked up some value to map roles in Sitecore. It could be enough for most use cases.
- Note 4: You can also map user profile properties, these are some examples.
Login Link
Since this is a website, by default you have no way to test this integration. You can set up a custom page to generate the login link to test the integration:
In the controller:
using Sitecore.Abstractions;
using System.Linq;
using System.Web.Mvc;
namespace AzureB2CSitecoreFederated.Controllers
{
public class FederatedLoginController : Controller
{
private readonly BaseCorePipelineManager _pipelineManager;
public FederatedLoginController(BaseCorePipelineManager pipelineManager)
{
_pipelineManager = pipelineManager;
}
public ActionResult Index()
{
var args = new Sitecore.Pipelines.GetSignInUrlInfo.GetSignInUrlInfoArgs("website", "/");
Sitecore.Pipelines.GetSignInUrlInfo.GetSignInUrlInfoPipeline.Run(_pipelineManager, args);
ViewBag.SignInUrl = args.Result.FirstOrDefault()?.Href;
return View();
}
}
}
In the views
@using System.Web.Mvc;
@{using (Html.BeginForm(null, null, FormMethod.Post, new { action = ViewBag.SignInUrl }))
{
<button type="submit">
Login
</button>
}
}
<div>
<p>@Sitecore.Security.Authentication.AuthenticationManager.GetActiveUser().LocalName</p>
<p>Is Authed: @Sitecore.Context.User.IsAuthenticated</p>
<p>Name: @Sitecore.Context.User.Name</p>
<p>Localname: @Sitecore.Context.User.LocalName</p>
<p>Domain: @Sitecore.Context.User.GetDomainName()</p>
<p>Profile Email: @Sitecore.Context.User.Profile.Email</p>
<pre>
@Newtonsoft.Json.JsonConvert.SerializeObject(Sitecore.Context.User, Newtonsoft.Json.Formatting.Indented, new Newtonsoft.Json.JsonSerializerSettings
{
ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
})
</pre>
</div>
Skipped classes and configs for registering dependencies, you know how to do them.
That is all. In general, it's a pretty easy setup, always check logs and URL requests to identify issues and errors.