Mixed-mode authentication
Configure multiple authentication schemes in CMS 13 to use one provider for back-end users and another for website visitors.
Mixed-mode authentication lets a CMS 13 application use different identity providers for different audiences. For example, authenticate back-end editors with Entra ID through OpenID Connect while authenticating website visitors with ASP.NET Identity.
Configure multiple authentication schemes to enable mixed-mode authentication:
NoteBy default, the authentication middleware authenticates only the default scheme.
The default scheme becomes a-scheme in the following example because it is the first registered scheme.
services
.AddAuthentication()
.AddCookie("a-scheme")
.AddCookie("another-scheme");Change the default scheme by specifying it in the AddAuthentication("another-scheme") method or by configuring the schemes in the options class:
services
.AddAuthentication(options => {
options.DefaultScheme = "another-scheme";
options.DefaultAuthenticateScheme = "another-scheme";
});or:
services
.Configure<AuthenticationOptions>(options => {
options.DefaultScheme = "another-scheme";
options.DefaultAuthenticateScheme = "another-scheme";
});Users are signed in to multiple schemes simultaneously but authenticated individually. Each cookie scheme keeps the user signed in to its scheme. When adding an OpenID Connect provider, specify the cookie scheme that keeps the user signed in:
services
.AddAuthentication()
.AddCookie("a-scheme")
.AddCookie("another-scheme")
.AddOpenIdConnect("oidc", options => {
options.SignInScheme = "another-scheme";
options.SignOutScheme = "another-scheme";
…
});Each scheme has a different purpose. The authentication scheme authenticates the user, and the challenge scheme triggers a login flow for a specific scheme. The authenticate, sign-in, sign-out, and challenge methods accept an override to specify a scheme explicitly. When not specified, the default scheme is used.
The following example signs out of a specific scheme:
public async Task<IActionResult> Logout() {
await HttpContext.SignOutAsync("another-scheme");
return View();
}The AuthorizeAttribute also accepts a scheme:
[Authorize(AuthenticationSchemes = "oidc")]
public async Task<IActionResult> Index() {}
NoteThe cookie scheme keeps the user signed in, but authentication uses a different scheme.
More commonly, control which scheme to use with business logic, such as checking whether a specific cookie exists or whether the request targets a particular URL:
services
.AddAuthentication(options => {
options.DefaultScheme = "policy-scheme";
options.DefaultChallengeScheme = "policy-scheme";
})
.AddCookie("a-scheme")
.AddCookie("another-scheme")
.AddOpenIdConnect("oidc", options => {
options.SignInScheme = "another-scheme";
options.SignOutScheme = "another-scheme";
})
.AddPolicyScheme("policy-scheme", null, options => {
options.ForwardDefaultSelector = ctx => {
if (ctx.Request.Path.StartsWithSegments("/episerver", StringComparison.OrdinalIgnoreCase)) {
return "oidc";
}
return "a-scheme";
}
});For additional details, see Authorize with a specific scheme in ASP.NET Core.
Configure CMS services
Calling AddCmsAspNetIdentity<TUser>() adds a cookie authentication scheme called Identity.Application, which is the default name ASP.NET Identity uses.
CMS requires the following service implementations to manage users and roles in the UI:
SecurityEntityProvider– Provides users and roles to the UI when managing access rights to content.IQueryableNotificationUsers– Provides users when configuring content workflows.
When AddCmsAspNetIdentity<TUser>() is not called, CMS registers implementations that sync user information from external authentication providers.
The following code samples show how to sync users and roles from external providers. Choose one explicitly, or intercept the service to aggregate users from both implementations.
When AddCmsAspNetIdentity<TUser>() is called, the following code returns users and roles from both ASP.NET Identity and the synchronized external authentication provider.
In Startup.cs:
services.TryIntercept<SecurityEntityProvider>((s, inner) =>
new YourEntityProvider(
inner,
s.GetRequiredService<SynchronizingRolesSecurityEntityProvider>()));
/// <summary>
/// A security entity provider that aggregates the default provider with <see cref="SynchronizingRolesSecurityEntityProvider"/>
/// by decorating the default registered implementation. The entities are only aggregated when the default provider
/// is not <see cref="SynchronizingRolesSecurityEntityProvider"/>.
/// </summary>
public class YourSecurityEntityProvider: SecurityEntityProvider {
private readonly SecurityEntityProvider _innerSecurityEntityProvider;
private readonly SynchronizingRolesSecurityEntityProvider _synchronizingSecurityEntityProvider;
/// <inheritdoc />
public YourSecurityEntityProvider(
SecurityEntityProvider innerSecurityEntityProvider,
SynchronizingRolesSecurityEntityProvider synchronizingSecurityEntityProvider) {
_innerSecurityEntityProvider = innerSecurityEntityProvider;
_synchronizingSecurityEntityProvider = synchronizingSecurityEntityProvider;
}
/// <inheritdoc />
public override async Task<IEnumerable<string>> GetRolesForUserAsync(string userName) {
if (_innerSecurityEntityProvider is SynchronizingRolesSecurityEntityProvider) {
return await _synchronizingSecurityEntityProvider.GetRolesForUserAsync(userName);
}
var results = await Task.WhenAll(
_synchronizingSecurityEntityProvider.GetRolesForUserAsync(userName),
_innerSecurityEntityProvider.GetRolesForUserAsync(userName));
return results.SelectMany(x => x);
}
/// <inheritdoc />
public override async Task<(IEnumerable<string> users, int totalCount) > FindUsersInRoleAsync(string roleName, string usernameToMatch, int startIndex, int maxRows) {
if (_innerSecurityEntityProvider is SynchronizingRolesSecurityEntityProvider) {
return await _synchronizingSecurityEntityProvider.FindUsersInRoleAsync(roleName, usernameToMatch, startIndex, maxRows);
}
var results = await Task.WhenAll(
_synchronizingSecurityEntityProvider.FindUsersInRoleAsync(roleName, usernameToMatch, startIndex, maxRows),
_innerSecurityEntityProvider.FindUsersInRoleAsync(roleName, usernameToMatch, startIndex, maxRows));
return (results.SelectMany(x => x.users), results.Sum(x => x.totalCount));
}
/// <inheritdoc />
public override async Task<IEnumerable<SecurityEntity>> SearchAsync(string partOfValue, string claimType) {
return (await SearchAsync(partOfValue, claimType, 0, int.MaxValue)).entities;
}
/// <inheritdoc />
public override async Task<(IEnumerable<SecurityEntity> entities, int totalCount)> SearchAsync(string partOfValue, string claimType, int startIndex, int maxRows) {
if (_innerSecurityEntityProvider is SynchronizingRolesSecurityEntityProvider) {
return await _synchronizingSecurityEntityProvider.SearchAsync(partOfValue, claimType, startIndex, maxRows);
}
var results = await Task.WhenAll(
_synchronizingSecurityEntityProvider.SearchAsync(partOfValue, claimType, startIndex, maxRows),
_innerSecurityEntityProvider.SearchAsync(partOfValue, claimType, startIndex, maxRows));
return (results.SelectMany(x => x.entities), results.Sum(x => x.totalCount));
}
}Sync users and roles from an external authentication provider
Use ISynchronizingUserService to sync users and roles from an external provider when a user signs in. The following example syncs on the OnSignedIn event of a cookie scheme linked to an OpenID Connect provider:
services
.AddAuthentication()
.AddCookie("a-scheme")
.AddCookie("another-scheme", options => {
options.Events.OnSignedIn = async ctx => {
if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity) {
var synchronizingUserService = ctx
.HttpContext
.RequestServices
.GetRequiredService<ISynchronizingUserService>();
await synchronizingUserService.SynchronizeAsync(claimsIdentity);
}
};
})
.AddOpenIdConnect("oidc", options => {
options.SignInScheme = "another-scheme";
options.SignOutScheme = "another-scheme";
});Updated 17 days ago
