OpenID Connect and Entra ID in Commerce Connect
Describes OpenID Connect and Entra ID to manage the sign-in of users to the front-end site in Optimizely Content Management System (CMS) and to Optimizely Commerce Connect 13.
This topic explains configuring mixed mode authentication with OpenID Connect and Membership provider. That is, how a Commerce Connect application can authenticate admins/editors and shoppers/users through different authentication components. To accomplish these goals, you need to
- use OpenID Connect to sign-in admins from a single/multi-tenant environment. To accomplish this, use the ASP.Net OpenID Connect OWIN middleware.
- use a built-in Membership provider to authenticate users/shoppers.
You need to follow these steps in both your front-end site and in Commerce Manager.
About Entra ID and OpenID
Entra ID (formerly Azure Active Directory (Azure AD)) is Microsoft's multi-tenant cloud-based directory and identity management service. Entra IDprovides single sign-on (SSO) access to many cloud-based SaaS applications, and includes a full suite of identity management capabilities.
OAuth is an open standard for authorization used by Entra ID. OpenID Connect is built on top of OAuth and extends it. You can use OpenID Connect as an authentication protocol rather than just an authorization protocol.
For more information about the protocols, Authorize access to web applications using OpenID Connect and Entra ID.
Prerequisites
Create and configure an Entra ID Application
-
Follow this guide as a template for creating your Entra ID Application to use with OpenID Connect: Register your application with your Entra ID tenant.
-
Update the AD Application manifest to include the admin roles of your preference: Add application roles in Entra ID.
Configure both sites to support HTTPS
- Follow this guide to enable HTTPS in Visual Studio: Enable SSL in IIS Express from Visual Studio.
- Make sure not to use system or well-known ports. See Wikipedia Registered port for suggestions on port numbers available.
Configure mixed mode authentication in the front-end site
1. Disable the Role Provider
In web.config, disable the built-in Role provider. But leave the profile system enabled, since edit and admin views use it for language settings. You can use another profile system on the website.
<authentication mode="None" />
...
<roleManager enabled="false">
<providers>
<clear />
</providers>
</roleManager>
...
<membership>
<providers>
<clear />
</providers>
</membership>
2. Configure Optimizely to support claims
Enable claims on virtual roles by setting the addClaims property. Also, add the provider SynchronizingRolesSecurityEntityProvider for security entities, which is used by the set access rights dialog box, content approval, and impersonating users among other things.
Users and groups are synchronized to custom Optimizely tables in the database when a user is authenticated (see ISynchronizingUserService in the code example below). There is no background synchronization, so for a role change to take effect, the user must login to the site again.
<episerver.framework>
<securityEntity>
<providers>
<add name="SynchronizingProvider"
type="EPiServer.Security.SynchronizingRolesSecurityEntityProvider, EPiServer"/>
</providers>
</securityEntity>
<virtualRoles addClaims="true">
//existing virtual roles
</virtualRoles>
You can also replace virtual roles with roles defined in the manifest to delegate this control from the application to Azure. See Add application roles in Entra ID.
3. Install NuGet packages
In Visual Studio, open Package Manager and install the following packages.
Install-Package Microsoft.Owin.Security.Cookies
Install-Package Microsoft.Owin.Security.OpenIdConnect
Install-Package Microsoft.Owin.Host.SystemWeb
Update-Package Microsoft.IdentityModel.Protocol.Extensions -Safe
Note
Always use Microsoft.IdentityModel.Protocol.Extensions package version 1.0.2 or later. Previous versions contain a critical bug that might cause threads to hang. The Katana team is working hard on updating performance and security, however sometimes bugs are logged. Visit the Katana teams Github page to stay on top of issues that could affect your implementation.
4. Configure the Startup.cs class to support mixed-mode authentication
To configure the OpenID Connect and Membership provider, replace the code in the startup class for OWIN middleware with the following example. The SecurityTokenValidated-event is used to synchronize the user and group membership to Episerver. You can also use this event for custom logic (for example, to add custom data to the user profile).
using EPiServer.Cms.UI.AspNetIdentity;
using EPiServer.Core;
using EPiServer.Reference.Commerce.Shared.Identity;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web.Routing;
using Mediachase.Data.Provider;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.IdentityModel.Protocols;
using Microsoft.Owin;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Configuration;
using System.Globalization;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Helpers;
public class Startup
{
private readonly IConnectionStringHandler _connectionStringHandler;
public Startup() : this(ServiceLocator.Current.GetInstance<IConnectionStringHandler>())
{
// Parameterless constructor required by OWIN.
}
public Startup(IConnectionStringHandler connectionStringHandler)
{
_connectionStringHandler = connectionStringHandler;
}
public void Configuration(IAppBuilder app)
{
app.AddCmsAspNetIdentity<SiteUser>(new ApplicationOptions
{
ConnectionStringName = _connectionStringHandler.Commerce.Name
});
// Enables the application to temporarily store user information when they are
// verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process
// will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ExternalCookie);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
CookieName = ".AspNet." + DefaultAuthenticationTypes.ExternalCookie,
ExpireTimeSpan = TimeSpan.FromMinutes(5)
});
// <add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}" />
string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
// <add key="ida:ClientId" value="Client ID from Azure AD application" />
string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
// <add key="ida:Tenant" value="Azure Active Directory, Directory Eg. Contoso.onmicrosoft.com/" />
string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
//<add key="ida:PostLogoutRedirectUri" value="https://the logout post uri/" />
string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
const string LogoutPath = "/Login/SignOut";
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"],
PostLogoutRedirectUri = postLogoutRedirectUri,
Scope = OpenIdConnectScopes.OpenIdProfile,
ResponseType = OpenIdConnectResponseTypes.IdToken,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
RoleClaimType = ClaimTypes.Role
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Write(context.Exception.Message);
return Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
// Here you can change the return uri based on multisite
HandleMultiSiteReturnUrl(context);
// To avoid a redirect loop to the federation server send 403
// when user is authenticated but does not have access
if (context.OwinContext.Response.StatusCode == 401 &&
context.OwinContext.Authentication.User.Identity.IsAuthenticated)
{
context.OwinContext.Response.StatusCode = 403;
context.HandleResponse();
}
return Task.FromResult(0);
},
SecurityTokenValidated = context =>
{
var redirectUri = new Uri(context.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
if (redirectUri.IsAbsoluteUri)
{
context.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
}
//Sync user and the roles to EPiServer in the background
ServiceLocator.Current.GetInstance<ISynchronizingUserService>().
SynchronizeAsync(context.AuthenticationTicket.Identity);
return Task.FromResult(0);
}
}
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password
// or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager<SiteUser>, SiteUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => manager.GenerateUserIdentityAsync(user)),
OnApplyRedirect = (context => context.Response.Redirect(context.RedirectUri)),
OnResponseSignOut = (context => context.Response.Redirect(UrlResolver.Current.GetUrl(ContentReference.StartPage)))
}
});
app.UseStageMarker(PipelineStage.Authenticate);
app.Map(LogoutPath, map =>
{
map.Run(ctx =>
{
if (OpenIdConnectUser(ctx.Authentication.User))
{
ctx.Authentication.SignOut();
}
else
{
ctx.Get<ApplicationSignInManager<SiteUser>>().SignOut();
}
return Task.FromResult(0);
});
});
// If the application throws an antiforgerytoken exception like
// “AntiForgeryToken: A Claim of Type NameIdentifier or
// IdentityProvider Was Not Present on Provided ClaimsIdentity”,
// set AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier.
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}
private bool OpenIdConnectUser(ClaimsPrincipal user)
{
const string openIdConnectProviderClaimType = "http://schemas.microsoft.com/identity/claims/identityprovider";
const string aspNetIdentityProviderClaimType = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider";
if (user.Claims.Any(c => c.Type == aspNetIdentityProviderClaimType))
{
return false;
}
if (user.Claims.Any(c => c.Type == openIdConnectProviderClaimType))
{
return true;
}
return false;
}
private void HandleMultiSiteReturnUrl(
RedirectToIdentityProviderNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> context)
{
// here you change the context.ProtocolMessage.RedirectUri to corresponding siteurl
// this is a sample of how to change redirecturi in the multi-tenant environment
if (context.ProtocolMessage.RedirectUri == null)
{
var currentUrl = EPiServer.Web.SiteDefinition.Current.SiteUrl;
context.ProtocolMessage.RedirectUri = new UriBuilder(
currentUrl.Scheme,
currentUrl.Host,
currentUrl.Port,
HttpContext.Current.Request.Url.AbsolutePath).ToString();
}
}
}
Configure OpenID Connect in Commerce Manager
Note
OpenID Connect with Entra ID delegates authentication of users with the right roles defined in the Entra ID Application Manifest. As Commerce Manager is business-critical software, we recommend not using it in mixed-mode authentication.
1. Configure the web.config file and install the same packages as above
See 2 and 3 above.
2. Configure the Commerce Manager Startup.cs class to use OpenID Connect
To configure OpenID Connect, replace the code in the startup class for OWIN middleware with the following example.
using EPiServer.Security;
using EPiServer.ServiceLocation;
using Microsoft.AspNet.Identity;
using Microsoft.IdentityModel.Protocols;
using Microsoft.Owin;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Configuration;
using System.Globalization;
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
public class Startup
{
public Startup()
{
// Parameterless constructor required by OWIN.
}
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ExternalCookie);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider.
// Configure the sign in cookie.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
CookieName = ".AspNet." + DefaultAuthenticationTypes.ExternalCookie,
ExpireTimeSpan = TimeSpan.FromMinutes(5)
});
// <add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}" />
string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
// <add key="ida:ClientId" value="Client ID from Entra ID application" />
string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
// <add key="ida:Tenant" value="Entra ID, Directory Eg. Contoso.onmicrosoft.com/" />
string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
//<add key="ida:PostLogoutRedirectUri" value="https://the logout post uri/" />
string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
const string LogoutPath = "/Apps/Shell/Pages/Logout.aspx";
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"],
PostLogoutRedirectUri = postLogoutRedirectUri,
Scope = OpenIdConnectScopes.OpenIdProfile,
ResponseType = OpenIdConnectResponseTypes.IdToken,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
RoleClaimType = ClaimTypes.Role
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Write(context.Exception.Message);
return Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
// Here you can change the return uri based on multisite
HandleMultiSiteReturnUrl(context);
// To avoid a redirect loop to the federation server send 403
// when user is authenticated but does not have access
if (context.OwinContext.Response.StatusCode == 401 &&
context.OwinContext.Authentication.User.Identity.IsAuthenticated)
{
context.OwinContext.Response.StatusCode = 403;
context.HandleResponse();
}
return Task.FromResult(0);
},
SecurityTokenValidated = (ctx) =>
{
var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
if (redirectUri.IsAbsoluteUri)
{
ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
}
//Sync user and the roles to EPiServer in the background
ServiceLocator.Current.GetInstance<ISynchronizingUserService>().
SynchronizeAsync(ctx.AuthenticationTicket.Identity);
return Task.FromResult(0);
}
}
});
app.UseStageMarker(PipelineStage.Authenticate);
app.Map(LogoutPath, map =>
{
map.Run(ctx =>
{
ctx.Authentication.SignOut();
return Task.FromResult(0);
});
});
}
private void HandleMultiSiteReturnUrl(
RedirectToIdentityProviderNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> context)
{
// here you change the context.ProtocolMessage.RedirectUri to corresponding siteurl
// this is a sample of how to change redirecturi in the multi-tenant environment
if (context.ProtocolMessage.RedirectUri == null)
{
var currentUrl = EPiServer.Web.SiteDefinition.Current.SiteUrl;
context.ProtocolMessage.RedirectUri = new UriBuilder(
currentUrl.Scheme,
currentUrl.Host,
currentUrl.Port,
HttpContext.Current.Request.Url.AbsolutePath).ToString();
}
}
}
Add application roles in Entra ID
By default, you need to declare application roles, such as WebEditors and WebAdmins, in the active directory application. Either the application owner (developer of the app) or the global administrator of the developer’s directory can declare roles for an application.
-
In the Azure portal, choose your Entra ID tenant by selecting it from the top right corner of the page (Click your name. In the dropdown, you will see Directory. Select the Entra ID directory where you created the AD application).
-
Select Azure Active Directory (Entra ID) extension from the left navigation panel and click App Registrations.
-
Click to open the application for which you want to declare application roles.
-
From the application page, click Manifest to open the inline manifest editor. From here, you can choose to download the manifest or upload a modified copy if you would rather use another editor.
-
Locate the appRoles setting and insert the appRole definitions in the array.
Below is an example of approles that declares WebAdmins and WebEditors. Modify it according to your application roles.Note
You need to generate a new Guid for each role declaration.
"appRoles": [ { "allowedMemberTypes": [ "User" ], "description": "Editor can edit the site.", "displayName": "WebEditors", "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "isEnabled": true, "value": "WebEditors" }, { "allowedMemberTypes": [ "User" ], "description": "Admins can manage roles and perform all task actions.", "displayName": "WebAdmins", "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX", "isEnabled": true, "value": "WebAdmins" }, { "allowedMemberTypes": [ "User" ], "description": "Admin the site.", "displayName": "Administrators", "id": "XXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX", "isEnabled": true, "value": "Administrators" } ],
-
Upload the edited manifest using the inline manifest editor or save your changes if you used the inline manifest editor. For information, see Understand the Entra ID application manifest.
Assign users and groups to roles in the active directory application
To assign users and groups to specific roles in your active directory application, you need to:
- Go to the registered Entra ID application you have configured with OpenId Connect.
- On the AD application main page, click Managed application in local directory.
- In the left panel, click User and Groups.
- Click Add User.
- Select or invite a user from Users.
- From Select Role, select a role to assign.
- Click Assign.
Note
If a user is assigned multiple roles, repeat this process for each role.
Caution
Known issue: If the application throws an antiforgerytoken exception like AntiForgeryToken: A Claim of Type NameIdentifier or IdentityProvider Was Not Present on Provided ClaimsIdentity, set AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier.
Updated 3 months ago