Mixed-mode authentication
Describes a common use case for mixed-mode authentication, having one authentication provider for your back-end users and another provider for website users.
Mixed-mode authentication is achieved y configuring multiple authentication schemes:
Note
By default, the authentication middleware will authenticate only the default scheme.
This becomes 'a-scheme' in the following example because it is the first registered one.
services
.AddAuthentication()
.AddCookie("a-scheme")
.AddCookie("another-scheme");
You can change this by specifying a default scheme in the AddAuthentication("another-scheme")
method directly or by configuring the schemes more granularly 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 can be signed in to multiple schemes simultaneously but authenticated individually. Multiple cookie schemes are needed; each cookie scheme keeps the user signed in to a scheme. For example, if you add an OpenID Connect provider, you specify the cookie scheme to use for keeping that 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. For example, the authenticate scheme is used to authenticate the user, and the challenge scheme triggers a login flow for a specific scheme. Authenticating, signing-in, signing-out, and challenging methods have an override where one can explicitly specify which scheme to use. If not specified, the default scheme is used.
Example of how to use a specific scheme:
public async Task<IActionResult> Logout() {
await HttpContext.SignOutAsync("another-scheme");
return View();
}
The AuthorizeAttribute
can also specify which scheme to use:
[Authorize(AuthenticationSchemes = "oidc")]
public async Task<IActionResult> Index() {}
Note
The cookie scheme keeps the user signed in, but you authenticate with another scheme.
More commonly, you want to spontaneously control which scheme to use with your business logic, such as checking if a specific cookie exists or if the request is to 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";
}
});
See Authorize with a specific scheme in ASP.NET Core for information on which scheme to use.
Configure CMS services
If you call AddCmsAspNetIdentity<TUser>()
, a cookie authentication scheme called Identity.Application
is added. (This is the default name ASP.NET Identity uses.)
Optimizely Content Management System (CMS) expects several implementations for services also to be registered, most importantly,SecurityEntityProvider
and IQueryableNotificationUsers
.
SecurityEntityProvider
is used by CMS to provide users and roles to the UI when managing access rights to content, for example.IQueryableNotificationUsers
is used when configuring content workflows.
If you do not call AddCmsAspNetIdentity<TUser>()
, CMS registers two other implementations that sync user information from external authentication providers.
The following code samples show how to sync users and roles from external providers. You might need to choose one of these explicitly, or if you want users from implementations, you can intercept the service and then add users from the other implementation.
If you call AddCmsAspNetIdentity<TUser>()
, the following code ensures that users and roles are returned from ASP.NET Identity and the synchronized users and roles from the 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));
}
}
Synchronize users and roles from an external authentication 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 5 months ago