Sanjay Kumar
+3
Nov 28, 2025
visibility 1128
star star star star star
(1 votes)

Using Okta and OpenID Connect with Optimizely CMS 12

Modern CMS solutions rarely live in isolation. Your editors already log into other systems with SSO, and they expect the same from Optimizely CMS. In this post, we’ll look at how to integrate Okta and OpenID Connect into an Optimizely CMS 12 site, using a real-world implementation.

We’ll cover:

  • Wiring Okta into ASP.NET Core authentication
  • Enabling/disabling Okta per environment via configuration
  • Mapping Okta claims to Optimizely-friendly identities
  • Handling login, logout, and post-logout flows

1. Configuration: feature flag + Okta settings
First, the site treats Okta as a feature, controlled by configuration. In appsettings.Development.json, you’ll see a dedicated Okta section:

"Okta": {
  "Enabled": false,
  "Domain": "",
  "ClientId": "",
  "ClientSecret": ""
},

This gives you:

  • Enabled: a simple on/off switch per environment
  • Domain, ClientId, ClientSecret: the standard Okta OIDC settings

In Startup.cs, these values decide whether the site runs with Okta SSO or falls back to a simple local admin registration:

bool.TryParse(_configuration["okta:Enabled"], out bool oktaEnabled);
if (oktaEnabled)
{
    services.SetupOkta(_configuration, syncUser: SyncUseDetails);
}
else
{
    services.AddAdminUserRegistration(x => x.Behavior = EPiServer.Cms.Shell.UI.RegisterAdminUserBehaviors.SingleUserOnly);
}
   private static void SyncUserDetails(ClaimsIdentity identity)
   {
       ServiceLocator.Current.GetInstance<ISynchronizingUserService>().SynchronizeAsync(identity);
       Infrastructure.Async.AsyncHelper.RunSync(async () =>
       {
           //TODO :  SyncUserProfile(identity);
           //TODO :  SyncRolesAsync(identity);
       });
   }

 

This pattern makes it easy to run local/dev environments without needing full Okta wiring, while production gets full SSO.

2. Wiring Okta + cookies into ASP.NET Core authentication
The core of the integration lives in an extension method on IServiceCollection, which sets up:

  • Cookie authentication as the primary auth scheme
  • Okta MVC as the OpenID Connect challenge handler
  • Custom login/logout paths
  • Claim mapping and error handling
    public static class IServiceCollectionExtensions
    {
        public static IServiceCollection SetupOkta(this IServiceCollection services,
            IConfiguration configuration,
            Action<ClaimsIdentity> syncUser)
        {
            services
                .ConfigureApplicationCookie(options =>
                {
                    options.LoginPath = new PathString("/account/login");
                    options.LogoutPath = "/account/logout";
                })
                .AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
                })
                .AddCookie(x =>
                {
                    x.SlidingExpiration = true;
                    x.Cookie.HttpOnly = true;
                    x.ExpireTimeSpan = TimeSpan.FromMinutes(30);
                })
                .AddOktaMvc(new OktaMvcOptions
                {
                    OktaDomain = configuration["okta:Domain"],
                    ClientId = configuration["okta:ClientId"],
                    ClientSecret = configuration["okta:ClientSecret"],
                    Scope = new List<string> { "openid", "profile", "email", "groups" },
                    PostLogoutRedirectUri = "/account/postlogout",
                    GetClaimsFromUserInfoEndpoint = true,
                    OpenIdConnectEvents = new OpenIdConnectEvents
                    {
                        OnTokenValidated = async (ctx) =>
                        {
                            if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
                            {
                                if (claimsIdentity != null)
                                {
                                    var authClaimsIdentity = MappedClaims(ctx.Principal);
                                    syncUser?.Invoke(authClaimsIdentity);
                                    ctx.Principal = new ClaimsPrincipal(authClaimsIdentity);
                                }
                            }
                        },
                        OnRemoteFailure = context =>
                        {
                            if (context.Failure is OpenIdConnectProtocolException oidcException)
                            {
                                var log = LogManager.GetLogger();
                                log.Error($"Remote login OpenIdConnectProtocolException: {Uri.EscapeDataString(oidcException.Message)}");
                                context.HandleResponse();
                                context.Response.Redirect("/account/login?error=authentication_failed"); 
                            }
                            else if (context.Failure is Exception e)
                            {
                                var log = LogManager.GetLogger();
                                log.Error($"Remote login Exception: {Uri.EscapeDataString(e.Message)}");
                                context.HandleResponse();
                               context.Response.Redirect("/account/login?error=authentication_failed"); 
                            }

                            return Task.CompletedTask;
                        }
                    }
                });

            services.PostConfigureAll<OpenIdConnectOptions>(options =>
            {
                if (options.TokenValidationParameters != null)
                {
                    options.TokenValidationParameters.RoleClaimType = "optimizelyGroups";
                    options.TokenValidationParameters.NameClaimType = "email";
                }
            });
            return services;
        }

Key Points:

  • Cookies remain the primary session mechanism inside the CMS, while Okta handles sign-in via OIDC.
  • DefaultChallengeScheme is OpenIdConnectDefaults.AuthenticationScheme, so any Challenge() will redirect to Okta.
  • Scope includes "groups", which is handy for mapping Okta groups into Optimizely roles.
  • PostLogoutRedirectUri is handled by an MVC action (shown below).

The PostConfigureAll<OpenIdConnectOptions> step ensures that:

  • RoleClaimType is optimizelyGroups (matching how Optimizely expects role data)
  • NameClaimType is email, which is typically what you want in corporate setups

3. Mapping Okta claims to Optimizely-friendly identities
Rather than using Okta’s raw claim set, the site reshapes claims into something friendlier for Optimizely and downstream services.

 private static ClaimsIdentity MappedClaims(ClaimsPrincipal claimsPrincipal)
 {
            var authClaimsIdentity = new ClaimsIdentity(claimsPrincipal.Claims, claimsPrincipal?.Identity?.AuthenticationType, "sub", ClaimTypes.Role);
            var name = claimsPrincipal?.Claims?.FirstOrDefault(x => x.Type == "name")?.Value;
            var email = claimsPrincipal?.Claims?.FirstOrDefault(x => x.Type == "email")?.Value;
            var userId = claimsPrincipal?.Claims?.FirstOrDefault(x => x.Type == "sub")?.Value ?? email;
            var nameAry = name?.Split(" ", StringSplitOptions.RemoveEmptyEntries);

            if (nameAry != null && nameAry.Length > 0 && !string.IsNullOrEmpty(userId) && !string.IsNullOrEmpty(email))
            {
                var givenName = nameAry[0];
                var surName = string.Empty;
                if (nameAry.Length >= 2)
                {
                    surName = nameAry[1];
                }
                else
                {
                    surName = nameAry[2];
                }
                authClaimsIdentity.AddClaim(new Claim(ClaimTypes.Name, userId));
                authClaimsIdentity.AddClaim(new Claim(ClaimTypes.Email, email));
                authClaimsIdentity.AddClaim(new Claim(ClaimTypes.GivenName, givenName));
                authClaimsIdentity.AddClaim(new Claim(ClaimTypes.Surname, surName));
            }
            return authClaimsIdentity;
  }

This function:

  • Chooses a stable userId (subject or email)
  • Parses the Okta name claim into first/last name
  • Adds standard ClaimTypes (Name, Email, GivenName, Surname) that are widely used across .NET and Optimizely APIs

You also get a hook via the syncUser delegate passed into SetupOkta, so you can:

  • Create/update users in the Optimizely database
  • Sync roles based on Okta groups
  • Apply custom profile logic whenever a user logs in

4.MVC endpoints for login and logout
On the MVC side, login and logout are kept intentionally simple.
Login is just a challenge to the Okta MVC scheme if the user isn’t already authenticated:

  public IActionResult Login()
  {
      var userIdentity = HttpContext.User.Identity;
      if (userIdentity == null || !userIdentity.IsAuthenticated)
       {
         return Challenge(OktaDefaults.MvcAuthenticationScheme);
       }
      return Redirect("/");
 }

 Logout signs out of your local app session and, if appropriate, from Okta/OpenID Connect as well:

public IActionResult Logout()
{
            _userService.SignOut();
            var userIdentity = HttpContext.User.Identity;
            if (userIdentity != null &&
                userIdentity.IsAuthenticated &&
                !string.Equals("Identity.Application", userIdentity.AuthenticationType))
            {
                return new SignOutResult(
                    new[] { CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme },
                    new AuthenticationProperties { RedirectUri = "/" });
            }

            return Redirect("/");
}

public IActionResult PostLogout()
{
    return Redirect("/");
}

The PostLogout action matches the PostLogoutRedirectUri configured in OktaMvcOptions, keeping the entire sign-out flow under your control.

5. Optimizely’s own OpenID Connect:

Finally:

      services.AddOpenIDConnect<SiteUser>(
                useDevelopmentCertificate: _webHostingEnvironment.IsDevelopment(),
                signingCertificate: null,
                encryptionCertificate: null,
                createSchema: true);

        services.AddOpenIDConnectUI();

This combination gives you:

  • Okta-based SSO for users in the CMS UI
  • Optimizely’s own OIDC infrastructure for API access, headless clients, and integration scenarios

Conclusion

  • Using configuration to toggle Okta per environment,
  • Wiring Okta + cookies into ASP.NET Core authentication,
  • Mapping claims into Optimizely-friendly identities,
  • And keeping login/logout flows simple and explicit,

You get a robust, testable, and production-ready Okta + OpenID Connect integration for Optimizely CMS 12.

Nov 28, 2025

Comments

error Please login to comment.
Latest blogs
Architecting an Enterprise-Grade Development Pipeline in Optimizely SaaS CMS

Most enterprise teams show up to Optimizely SaaS CMS with a clear roadmap for their release pipeline: DEV → QA → Stage → Prod. Four logical...

Vipin Banka | Jul 12, 2026

Bynder DAM Connector for Optimizely SaaS CMS: Improved Metadata Property Synchronization

While working with the Bynder DAM Connector for Optimizely SaaS CMS , one of the key areas I explored was how Bynder asset metadata is synchronized...

Vipin Banka | Jul 11, 2026

Optimizely DXP: Every Supported Culture, One Searchable Page

Quick one for anyone building multi-language sites on Optimizely DXP. I put together a reference tool listing all 806 supported cultures. More...

Adnan Zameer | Jul 10, 2026 |

A day in the life of an Optimizely OMVP: London Meetup 2026

On 2nd July 2026 the Optimizely London Developer Meetup returned to The Lightwell, and the running theme across the evening was less about individu...

Graham Carr | Jul 10, 2026

Optimizely’s Summer ’26 Roadmap: The CMS Is Starting to Look Less Like a Publishing Tool and More Like Marketing Infrastructure

Optimizely’s Summer ’26 Product Roadmap event was not just a list of product updates. At least, that is not the part I found most interesting. The...

Augusto Davalos | Jul 9, 2026

Optimizely Content JS SDK v2.1.0 — What's New and Why It Matters

  v2.1.0 of the Optimizely Content JS SDK and CLI landed on July 7, 2026. This is a substantial release bringing a wave of capabilities for...

Vipin Banka | Jul 8, 2026