Welcome to the navigation

Consectetur deserunt irure qui exercitation sit excepteur dolor sint dolore minim voluptate nisi ut sed amet, aliqua, reprehenderit veniam, ad sunt et consequat, in ex. Esse ut proident, deserunt sit quis anim cillum consequat, magna nulla veniam, mollit laboris ullamco nostrud ea ex irure dolor do et excepteur aliqua, ut

Yeah, this will be replaced... But please enjoy the search!

Handling Multiple OpenID Connect Providers in Optimizely CMS with .NET 8

When you need to authenticate against more than one OpenID Connect provider in the same ASP.NET Core app – say IdentityServer for the public site and Microsoft Entra ID for Optimizely CMS – you quickly run into challenges: duplicate cookie schemes, mismatched role claims, and broken callback paths.

Here’s the approach that finally worked cleanly in .NET 8.

The Goal

  • / → authenticates with IdentityServer (DuendeIdentity)

  • /episerver/** → authenticates with Azure AD / Entra

  • Both share one local sign-in cookie ("Smart") so [Authorize] and User.IsInRole() behave consistently.

Configuring the CMS

First and foremost you'll need to configure the CMS to allow other identityproviders to act. This is done by allowing everyone or authenticated users for the site root.

This is typically done in the Set Access Rights page in the CMS Admin gui, /EPiServer/EPiServer.Cms.UI.Admin/default#/AccessRights/SetAccessRights.

Implementing the Core Idea

Use a policy scheme to route challenges based on the request path, and make all remote handlers sign into the same cookie scheme.

services
.AddAuthentication(options =>
{
    options.DefaultScheme = "Smart";
    options.DefaultChallengeScheme = "dynamic-oidc";
})
.AddPolicyScheme("dynamic-oidc", "Select by url path", options =>
{
    options.ForwardDefaultSelector = context => 
        context.Request.Path.Value?.Contains("/episerver", StringComparison.CurrentCultureIgnoreCase) == true 
        ? "EntraID" 
        : "DuendeIdentity";
})
.AddOpenIdConnect("DuendeIdentity", options =>
{
    configuration.GetSection("DuendeIdentityServerOptions").Bind(options);
    
    options.RequireHttpsMetadata = false;
    options.CallbackPath = "/signin-oidc-duende";
    options.ResponseType = "code id_token"; // Hybrid flow
    options.GetClaimsFromUserInfoEndpoint = true;
    options.SaveTokens = true;

    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        RoleClaimType = ClaimConstants.Roles,
        NameClaimType = ClaimConstants.Name
    };
})
.AddMicrosoftIdentityWebApp(
    openIdConnectScheme: "EntraID",
    cookieScheme: "Smart",
    configureMicrosoftIdentityOptions: options =>
    {
        configuration.GetSection("EntraID").Bind(options);
        options.TokenValidationParameters = new TokenValidationParameters
        {
            RoleClaimType = ClaimConstants.Roles,
            NameClaimType = ClaimConstants.Name,
            ValidateIssuer = true
        };

        options.Scope.Clear();
        options.Scope.Add(OpenIdConnectScope.OfflineAccess); // if you need refresh tokens
        options.Scope.Add(OpenIdConnectScope.Email);
        options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
        options.MapInboundClaims = false;
    },
    configureCookieAuthenticationOptions: options =>
    {
        options.AccessDeniedPath = "/AccessDenied";

        options.Cookie.Domain = null;
        options.Cookie.Name = "__Host-MyApp";
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.DataProtectionProvider = DataProtectionProvider.Create("MyApp");

        options.Events.OnSigningIn = async ctx =>
        {
            if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
            {
                // Syncs user and roles so they are available to the CMS
                var synchronizingUserService = ctx
                    .HttpContext
                    .RequestServices
                    .GetRequiredService();

                await synchronizingUserService.SynchronizeAsync(claimsIdentity);
            };

            await Task.CompletedTask;
        };
    })
;

The config sections for IdentityServer and EntraID looks like this

"EntraID": {
  "Instance": "https://login.microsoftonline.com/",
  "Domain": "xxx.onmicrosoft.com",
  "TenantId": "xxx",
  "ClientId": "xxx",
  "ClientSecret": "xxx"
},
"DuendeIdentityServerOptions": {
  "Authority": "xxx",
  "ClientId": "xxx",
  "ClientSecret": "xxx",
  "Scopes": ["openid", "profile", "offline_access"]
},

 

Key Lessons

  1. Unique callback paths for each OIDC scheme (/signin-oidc-duende, /signin-oidc-azure).

  2. One cookie scheme shared by all ("Smart").

  3. Consistent role claim type across providers ("roles").

  4. PolicyScheme decides which provider handles a request.

With this setup, both IdentityServer4 and Entra ID work side-by-side, the correct IdP is chosen automatically, and authorization with [Authorize(Roles = …)] succeeds no matter which one signed you in.