Use Opti ID with a separate site-visitor authentication scheme
Opti ID authenticates Optimizely product users, such as editors and administrators in the CMS shell. Site visitors are out of scope for Opti ID and are unaffected by it. This topic describes how to run Opti ID for editors alongside a separate authentication scheme for visitors.
ASP.NET Identity support in CMS 13
- EPiServer.CMS.UI.AspNetIdentity ships in CMS 13 and is supported.
- There is no announced removal version.
- Enabling Opti ID does not require you to migrate accounts or password hashes. Existing visitor accounts in AspNetUsers keep working.
Prerequisites
- If you are upgrading from CMS 12, see Upgrade to CMS 13 from CMS 12.
- Familiarity with Mixed-mode authentication
Visitors use ASP.NET Identity, editors use Opti ID
Register both in ConfigureServices. AddCmsAspNetIdentity<TUser>() registers the Identity.Application cookie scheme for visitors. AddOptimizelyIdentity() registers the Opti ID scheme for editors.
public void ConfigureServices(IServiceCollection services) {
// Visitors: ASP.NET Identity (Identity.Application cookie)
services.AddCmsAspNetIdentity<SiteUser>();
// Editors and administrators: Opti ID
services.AddOptimizelyIdentity();
// Keep the SecurityEntityProvider interceptor from the section below
// so both populations appear in the admin UI.
services.TryIntercept<SecurityEntityProvider>((s, inner) =>
new CombinedSecurityEntityProvider(
inner,
s.GetRequiredService<SynchronizingRolesSecurityEntityProvider>()));
}Call AddOptimizelyIdentity() without useAsDefault: true. Do not pass useAsDefault: false either. See Cookies and useAsDefault.
Sign visitors in with the standard SignInManager<SiteUser> APIs. For external visitor logins, call SignInManager.ExternalLoginSignInAsync(...).
Visitors use a customer-owned OIDC or B2C provider
Use this pattern when visitors authenticate against your own OpenID Connect or Azure AD B2C provider rather than ASP.NET Identity. Register the visitor scheme as the default and keep Opti ID as a separate scheme.
public void ConfigureServices(IServiceCollection services) {
services
.AddAuthentication(options => {
options.DefaultScheme = "visitor-cookie";
options.DefaultChallengeScheme = "visitor-oidc";
})
.AddCookie("visitor-cookie", options => {
options.Cookie.Name = "site.visitor";
options.LoginPath = "/login";
})
.AddOpenIdConnect("visitor-oidc", options => {
options.Authority = "https://your-b2c-or-oidc-provider";
options.ClientId = "<visitor-client-id>";
options.ClientSecret = "<visitor-client-secret>";
options.ResponseType = "code";
options.SignInScheme = "visitor-cookie";
options.SignOutScheme = "visitor-cookie";
});
// Editors and administrators: Opti ID, registered without useAsDefault
services.AddOptimizelyIdentity();
}Opti ID keeps its own cookie, so visitor claims and CMS claims are never mixed. Without AddCmsAspNetIdentity<TUser>(), CMS registers implementations that synchronize user information from external providers. To sync visitors and roles on sign-in, see Sync users and roles from an external authentication provider.
Cookies and useAsDefault
Each scheme owns its own cookie. Opti ID writes only its own CMS cookie. It never writes Identity.Application.
Scheme | Cookie owner | Authenticates |
|---|---|---|
ASP.NET Identity (Identity.Application) | AddCmsAspNetIdentity<TUser>() | Site visitors |
Customer OIDC/B2C (for example visitor-cookie) | Your registration | Site visitors |
Opti ID (OptimizelyIdentityDefaults.AuthenticationScheme) | AddOptimizelyIdentity() | Editors and administrators in the CMS shell |
Setting | Effect |
|---|---|
useAsDefault omitted (recommended) | Opti ID owns the CMS shell. Your visitor scheme stays the application default, so the public site continues to authenticate visitors. |
useAsDefault: true | Opti ID becomes the application default scheme. Visitors can no longer use the default scheme, and the application can fall back to local sign-in. |
useAsDefault: false | Not recommended. Opti ID still writes only its own cookie, so a user signed in through Opti ID is authenticated in the edit or admin UI but anonymous on the public site. |
Show both populations in the admin UI
When you call AddCmsAspNetIdentity<TUser>(), the default SecurityEntityProvider returns only ASP.NET Identity users and roles. Opti ID users and roles are synchronized separately. Intercept SecurityEntityProvider so both populations appear when you set content access rights.
services.TryIntercept<SecurityEntityProvider>((s, inner) =>
new CombinedSecurityEntityProvider(
inner,
s.GetRequiredService<SynchronizingRolesSecurityEntityProvider>()));
public class CombinedSecurityEntityProvider : SecurityEntityProvider {
private readonly SecurityEntityProvider _inner;
private readonly SynchronizingRolesSecurityEntityProvider _synchronizing;
public CombinedSecurityEntityProvider(
SecurityEntityProvider inner,
SynchronizingRolesSecurityEntityProvider synchronizing) {
_inner = inner;
_synchronizing = synchronizing;
}
public override async Task<IEnumerable<string>> GetRolesForUserAsync(string userName) {
if (_inner is SynchronizingRolesSecurityEntityProvider) {
return await _synchronizing.GetRolesForUserAsync(userName);
}
var results = await Task.WhenAll(
_synchronizing.GetRolesForUserAsync(userName),
_inner.GetRolesForUserAsync(userName));
return results.SelectMany(x => x);
}
public override async Task<(IEnumerable<string> users, int totalCount)> FindUsersInRoleAsync(
string roleName, string usernameToMatch, int startIndex, int maxRows) {
if (_inner is SynchronizingRolesSecurityEntityProvider) {
return await _synchronizing.FindUsersInRoleAsync(roleName, usernameToMatch, startIndex, maxRows);
}
var results = await Task.WhenAll(
_synchronizing.FindUsersInRoleAsync(roleName, usernameToMatch, startIndex, maxRows),
_inner.FindUsersInRoleAsync(roleName, usernameToMatch, startIndex, maxRows));
return (results.SelectMany(x => x.users), results.Sum(x => x.totalCount));
}
public override async Task<IEnumerable<SecurityEntity>> SearchAsync(string partOfValue, string claimType) {
return (await SearchAsync(partOfValue, claimType, 0, int.MaxValue)).entities;
}
public override async Task<(IEnumerable<SecurityEntity> entities, int totalCount)> SearchAsync(
string partOfValue, string claimType, int startIndex, int maxRows) {
if (_inner is SynchronizingRolesSecurityEntityProvider) {
return await _synchronizing.SearchAsync(partOfValue, claimType, startIndex, maxRows);
}
var results = await Task.WhenAll(
_synchronizing.SearchAsync(partOfValue, claimType, startIndex, maxRows),
_inner.SearchAsync(partOfValue, claimType, startIndex, maxRows));
return (results.SelectMany(x => x.entities), results.Sum(x => x.totalCount));
}
}Protect custom routes
Shell modules authenticate against Opti ID automatically. For custom routes outside shell modules, such as a custom admin page, add the Opti ID scheme to the policy. Otherwise the request is challenged by the default (visitor) scheme and redirected to the local login.
services.AddAuthorization(options => {
options.AddPolicy("CustomAdmin", policy => {
policy.AddAuthenticationSchemes(OptimizelyIdentityDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireRole("CmsAdmins");
});
});[Authorize(Policy = "CustomAdmin")]
public class CustomAdminController : Controller { }Virtual roles
Opti ID automatically maps the CmsEditors and CmsAdmins roles. Remove any existing mappings for these roles, such as appsettings.json entries or code-based virtual role registrations, because they conflict with the Opti ID mappings.
The following are untouched by Opti ID and stay in AspNetRoles:
- Commerce virtual roles
- Bespoke customer roles
For more on virtual roles, see Virtual roles.
Troubleshooting
Symptom | Cause and fix |
|---|---|
Signed in to /ui/cms but anonymous on the public site | Opti ID writes only its own cookie and does not write Identity.Application. This is expected. Authenticate visitors through your visitor scheme. Do not call AddOptimizelyIdentity with useAsDefault: false expecting it to sign visitors in. |
useAsDefault: true falls back to local sign-in | Opti ID is replacing the application default scheme. Remove useAsDefault: true so your visitor scheme stays the default and Opti ID owns only the CMS shell. |
A custom admin route redirects to the local login | The route is challenged by the default scheme. Add policy.AddAuthenticationSchemes(OptimizelyIdentityDefaults.AuthenticationScheme) to the policy. |
Administer Users or Administer Groups returns HTTP 500 | Add the SecurityEntityProvider interceptor from Show both populations in the admin UI and confirm AddCmsAspNetIdentity<TUser>() is registered. |
What is not supported
- There is no built-in shared-password or audience-gated page mechanism for visitors.
- Audiences (visitor groups) are for personalization, not access control. Do not use them to restrict access to content.
To restrict content for visitors, use your visitor authentication scheme with authorization policies, or see Set access rights without Opti ID.