---
title: Mixed-mode authentication for CMS 13
slug: cms-13/docs/mixed-mode-authentication
docTags: 
createdAt: 2026-07-30T20:22:08.991Z
---

## Prerequisites

- [Configure Opti ID for CMS 13](https://docs.optimizely.com/opti-id/docs/configure-opti-id-for-cms-13)
- If you need to upgrade CMS 12 to CMS 13, see [Upgrade to CMS 13 from CMS 12](https://docs.optimizely.com/cms-13/docs/upgrade-to-cms-13)

## Configure mixed-mode authentication

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:

:::hint{type="info"}
**Note**
By 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.

```csharp
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:

```csharp
services
  .AddAuthentication(options => {
    options.DefaultScheme = "another-scheme";
    options.DefaultAuthenticateScheme = "another-scheme";
  });
```

or:

```csharp
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:

```csharp
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:

```csharp
public async Task<IActionResult> Logout() {
  await HttpContext.SignOutAsync("another-scheme");
  return View();
}
```

The `AuthorizeAttribute` also accepts a scheme:

```csharp
[Authorize(AuthenticationSchemes = "oidc")]
public async Task<IActionResult> Index() {}
```

:::hint{type="info"}
**Note**
The 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:

```csharp
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](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme).

## Opti ID as the back-end provider

Opti ID registers and owns its own scheme.

1. Register `AddCmsAspNetIdentity<SiteUser>()` for visitors and `AddOptimizelyIdentity()` for editors.
2. Call it without `useAsDefault:true`.
   Do not call with `useAsDefault:false` as Opti ID establishes the CMS authentication cookie only and does not write `Identity.Application`. This means a user signed in through Opti ID is authenticated in the edit or admin UI but is anonymous on the public site.
3. Call `SignInManager.ExternalLoginSignInAsync(...)`.
4. Optional: For custom routes outside shell modules, you need `policy.AddAuthenticationSchemes(OptimizelyIdentityDefaults.AuthenticationScheme)`.

## 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`:

```csharp
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:

```csharp
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";
  });
```
