HomeDev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunityDoc feedbackLog In

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 by configuring multiple authentication schemes:

By default, the authentication middleware will authenticate only the default scheme. In the following example, this becomes 'a-scheme' because it’s 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 at the same time, but can be authenticated one at a time. Therefore, 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 for authenticating the user and the challenge scheme is used for triggering a new login flow for a specific scheme. All methods for authenticating, signing-in, signing-out, and challenging have an override where one can explicitly specify which scheme to use. If not specified, the default scheme is used.

Example on 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 own business logic, such as checking if a specific cookie exists, or if the request is to a specific 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 more information on how to select 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 to also 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 synchronizes user information from external authentication providers.

The following code samples show how to synchronize users and roles from external providers. You might need to choose one of these explicitly, or if you want users from both 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 both 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";
    …
  });