How to Add Multiple Authentication Providers to Your Optimizely CMS 12 Site

How to Add Multiple Authentication Providers to Your Optimizely CMS 12 Site

Alex Rollin
Alex Rollin
October 29, 2025
Last updated : February 15, 2026
October 29, 2025

If you're building an Optimizely CMS 12 site that needs to support different types of users, you'll likely need multiple login options. Your marketing team might use Microsoft accounts, external partners could prefer Google login, and customers might want to create local accounts with just an email and password.

Setting up multiple authentication providers in Optimizely CMS 12 isn't complicated, but getting all the pieces to work together requires careful configuration. This guide walks you through adding Entra ID (formerly Azure AD), Google, Facebook, and local identity authentication to your site, while keeping the built-in CMS admin login fully functional.

Prerequisites

Before you start configuring authentication providers, make sure you have:

  • An Optimizely CMS 12 site running on .NET 6 or later
  • Admin access to your Entra ID tenant (if using Microsoft authentication)
  • Developer accounts for Google Cloud Console and/or Facebook Developer portal
  • Basic understanding of ASP.NET Core authentication middleware
  • Access to update your site's configuration files and deploy changes

We've learned that having test accounts ready for each provider saves significant debugging time later. Create dedicated test users in each system before you begin implementation.

Step-by-Step Implementation

Step 1: Install the Required NuGet Packages

Start by adding the authentication packages your site needs. Open your terminal in the project directory and run:

dotnet add package EPiServer.CMS.UI.AspNetIdentity
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
dotnet add package Microsoft.IdentityModel.Protocols.OpenIdConnect
dotnet add package Microsoft.AspNetCore.Authentication.Google
dotnet add package Microsoft.AspNetCore.Authentication.Facebook

These packages provide the foundation for local identity management and OAuth2/OpenID Connect integration with external providers.

Step 2: Configure Local Identity for CMS Users

Local identity handles the traditional email/password logins at /util/login. Add this configuration to your Startup.cs or Program.cs:

services.AddCmsAspNetIdentity<SiteUser>(options => {
    options.ConnectionStringOptions = new ConnectionStringOptions {
        Name = "EPiServerDB",
        ConnectionString = Configuration.GetConnectionString("EPiServerDB")
    };
});

This keeps your existing CMS admin accounts working exactly as before. The SiteUser class should inherit from IdentityUser and include any custom properties you need for your users.

Step 3: Set Up Entra ID Authentication

Entra ID (Microsoft's cloud identity service) typically serves as the primary authentication method for business users. Here's the complete configuration:

services.AddAuthentication()
    .AddCookie("azure-cookie", options => {
        options.Events = new CookieAuthenticationEvents {
            OnSignedIn = async ctx => {
                if (ctx.Principal?.Identity is ClaimsIdentity id) {
                    var sync = ctx.HttpContext.RequestServices
                        .GetRequiredService<ISynchronizingUserService>();
                    await sync.SynchronizeAsync(id);
                }
            }
        };
    })
    .AddOpenIdConnect("azure", options => {
        options.SignInScheme = "azure-cookie";
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.UsePkce = true;
        options.ClientId = Configuration["Authentication:AzureClientID"];
        options.ClientSecret = Configuration["Authentication:AzureClientSecret"];
        options.Authority = Configuration["Authentication:AzureAuthority"];
        options.CallbackPath = new PathString("/signin-oidc");
        
        // Configure scopes
        options.Scope.Clear();
        options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
        options.Scope.Add(OpenIdConnectScope.OfflineAccess);
        options.Scope.Add(OpenIdConnectScope.Email);
        
        // Map claims correctly
        options.MapInboundClaims = false;
        options.GetClaimsFromUserInfoEndpoint = true;
        options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
        options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
        options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
        
        // Set token validation parameters
        options.TokenValidationParameters = new TokenValidationParameters {
            RoleClaimType = "roles",
            NameClaimType = "preferred_username",
            ValidateIssuer = false
        };
        
        // Sync user on successful authentication
        options.Events = new OpenIdConnectEvents {
            OnTokenValidated = ctx => {
                if (ctx.Principal?.Identity is ClaimsIdentity id) {
                    id.AddClaim(new Claim("auth_provider", "entra"));
                }
                ServiceLocator.Current.GetInstance<ISynchronizingUserService>()
                    .SynchronizeAsync(ctx.Principal?.Identity as ClaimsIdentity);
                return Task.CompletedTask;
            }
        };
    });

Pay attention to the ISynchronizingUserService call. This ensures external users get proper CMS permissions based on their claims.

Step 4: Add Google Authentication

Google authentication configuration is more straightforward than Entra ID:

services.AddAuthentication()
    .AddGoogle("google", options => {
        options.SignInScheme = "azure-cookie"; // Share the same cookie
        options.ClientId = Configuration["Authentication:Google:ClientId"];
        options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
        
        // Map Google claims to standard claim types
        options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
        options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
        options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
        
        options.Events = new OAuthEvents {
            OnCreatingTicket = ctx => {
                if (ctx.Principal?.Identity is ClaimsIdentity id) {
                    id.AddClaim(new Claim("auth_provider", "google"));
                    
                    // Sync user with CMS
                    var sync = ctx.HttpContext.RequestServices
                        .GetRequiredService<ISynchronizingUserService>();
                    sync.SynchronizeAsync(id);
                }
                return Task.CompletedTask;
            }
        };
    });

Notice how we're using the same azure-cookie for the sign-in scheme. This allows users to stay logged in regardless of which provider they used.

Step 5: Configure Facebook Authentication

Facebook follows a similar pattern but requires specific scope and field configuration:

services.AddAuthentication()
    .AddFacebook("facebook", options => {
        options.SignInScheme = "azure-cookie";
        options.AppId = Configuration["Authentication:Facebook:AppId"];
        options.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
        
        // Facebook requires explicit scope configuration
        options.Scope.Clear();
        options.Scope.Add("email");
        options.Scope.Add("public_profile");
        
        // Specify which fields to retrieve
        options.Fields.Add("email");
        options.Fields.Add("first_name");
        options.Fields.Add("last_name");
        options.Fields.Add("name");
        
        // Map Facebook fields to claims
        options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
        options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "first_name");
        options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "last_name");
        
        options.Events = new OAuthEvents {
            OnCreatingTicket = ctx => {
                if (ctx.Principal?.Identity is ClaimsIdentity id) {
                    id.AddClaim(new Claim("auth_provider", "facebook"));
                    
                    var sync = ctx.HttpContext.RequestServices
                        .GetRequiredService<ISynchronizingUserService>();
                    sync.SynchronizeAsync(id);
                }
                return Task.CompletedTask;
            }
        };
    });

Step 6: Implement Smart Authentication Routing

With multiple providers configured, you need policy schemes to route authentication requests correctly:

services.AddAuthentication()
    .AddPolicyScheme("smart-auth", "Smart Auth", options => {
        options.ForwardDefaultSelector = ctx => {
            var cookies = ctx.Request.Cookies;
            
            // Check which cookie exists to determine the active session
            if (cookies.ContainsKey(".AspNetCore.azure-cookie")) 
                return "azure-cookie";
            if (cookies.ContainsKey(".AspNetCore.Identity.Application")) 
                return IdentityConstants.ApplicationScheme;
            
            // Default fallback
            return "azure-cookie";
        };
    })
    .AddPolicyScheme("smart-challenge", "Smart Challenge", options => {
        options.ForwardDefaultSelector = ctx => {
            var path = (ctx.Request.Path.Value ?? string.Empty).ToLowerInvariant();
            
            // Route based on the request path
            if (path.StartsWith("/login/google")) return "google";
            if (path.StartsWith("/login/facebook")) return "facebook";
            if (path.StartsWith("/login")) return "azure";
            if (path.StartsWith("/util/login")) return IdentityConstants.ApplicationScheme;
            
            return "azure"; // Default to Entra ID
        };
    });

// Set the default schemes
services.PostConfigure<AuthenticationOptions>(o => {
    o.DefaultScheme = "smart-auth";
    o.DefaultAuthenticateScheme = "smart-auth";
    o.DefaultChallengeScheme = "smart-challenge";
    o.DefaultSignInScheme = "azure-cookie";
});

This configuration intelligently routes authentication requests based on the URL path and existing cookies.

Step 7: Create Login Controller

Build a controller to handle the various login routes:

[Route("login")]
public class LoginController : Controller
{
    private readonly SignInManager<SiteUser> _signInManager;
    
    public LoginController(SignInManager<SiteUser> signInManager)
    {
        _signInManager = signInManager;
    }
    
    [HttpGet("")]
    public IActionResult Microsoft([FromQuery] string returnUrl = "/")
    {
        return Challenge(
            new AuthenticationProperties { RedirectUri = returnUrl }, 
            "azure"
        );
    }

    [HttpGet("google")]
    public IActionResult Google([FromQuery] string returnUrl = "/")
    {
        return Challenge(
            new AuthenticationProperties { RedirectUri = returnUrl }, 
            "google"
        );
    }

    [HttpGet("facebook")]
    public IActionResult Facebook([FromQuery] string returnUrl = "/")
    {
        return Challenge(
            new AuthenticationProperties { RedirectUri = returnUrl }, 
            "facebook"
        );
    }

    [HttpPost("logout")]
    public async Task<IActionResult> Logout()
    {
        // Check which provider was used
        var authProvider = User.FindFirst("auth_provider")?.Value;
        
        if (string.IsNullOrEmpty(authProvider))
        {
            // Local identity logout
            await _signInManager.SignOutAsync();
        }
        else
        {
            // External provider logout
            await HttpContext.SignOutAsync("azure-cookie");
            HttpContext.Response.Cookies.Delete(".AspNetCore.azure-cookie");
        }
        
        return Redirect("/");
    }
}

Step 8: Update Configuration Settings

Add all the necessary settings to your appsettings.json:

{
  "ConnectionStrings": {
    "EPiServerDB": "Server=.;Database=YourCmsDb;Trusted_Connection=True;TrustServerCertificate=True"
  },
  "Authentication": {
    "AzureClientID": "YOUR-ENTRA-CLIENT-ID",
    "AzureClientSecret": "YOUR-ENTRA-CLIENT-SECRET",
    "AzureAuthority": "https://login.microsoftonline.com/YOUR-TENANT-ID/v2.0",
    "CallbackPath": "/signin-oidc",
    "Google": {
      "ClientId": "YOUR-GOOGLE-CLIENT-ID",
      "ClientSecret": "YOUR-GOOGLE-CLIENT-SECRET"
    },
    "Facebook": {
      "AppId": "YOUR-FACEBOOK-APP-ID",
      "AppSecret": "YOUR-FACEBOOK-APP-SECRET"
    }
  }
}

Remember to store production secrets in Azure Key Vault or environment variables, not in your source code.

Step 9: Configure Provider Redirect URIs

Each provider needs the correct redirect URI configured in their respective portals:

Entra ID (Azure Portal):

  • Navigate to App Registrations → Your App → Authentication
  • Add redirect URI: https://your-domain.com/signin-oidc

Google Cloud Console:

  • Go to APIs & Services → Credentials → Your OAuth 2.0 Client
  • Add authorized redirect URI: https://your-domain.com/signin-google

Facebook Developer Console:

  • Open your app → Facebook Login → Settings
  • Add Valid OAuth Redirect URI: https://your-domain.com/signin-facebook

Common Mistakes to Avoid

Our experience shows that these issues trip up most developers when implementing multi-provider authentication:

1. Mismatched Redirect URIs

The redirect URI in your code must exactly match what's configured in the provider's portal. Even a trailing slash difference will cause authentication to fail. Double-check for:

  • HTTP vs HTTPS
  • WWW vs non-WWW
  • Trailing slashes
  • Case sensitivity (some providers care about this)

2. Forgetting User Synchronization

Without calling ISynchronizingUserService.SynchronizeAsync(), external users won't appear in the CMS with proper permissions. Always sync users during the authentication event.

3. Cookie Conflicts

Using different cookie schemes for each provider creates session management nightmares. Share a single cookie scheme across all external providers, but keep local identity separate.

4. Missing Claims Mapping

Each provider returns claims differently. If you don't map them correctly, you'll end up with users missing names or email addresses in your system.

5. Inadequate Error Handling

Authentication can fail for many reasons: expired secrets, network issues, or provider outages. Implement proper error pages and logging to help diagnose issues quickly.

Testing and Verification

After implementing authentication, thoroughly test each provider:

Test Checklist

1. Local Identity Testing

  • Navigate to /util/login
  • Sign in with a CMS admin account
  • Verify access to the CMS admin interface
  • Check that Quick Navigator works correctly

2. Entra ID Testing

  • Navigate to /login
  • Complete Microsoft sign-in flow
  • Verify user appears in CMS user management
  • Check assigned roles are correct

3. Google Testing

  • Navigate to /login/google
  • Complete Google authentication
  • Verify claims are mapped (name, email)
  • Test with a Gmail and Google Workspace account

4. Facebook Testing

  • Navigate to /login/facebook
  • Authenticate with Facebook
  • Verify email and name claims are present
  • Test with different privacy settings

5. Cross-Provider Testing

  • Log in with one provider
  • Log out
  • Log in with a different provider
  • Verify no session conflicts occur

6. Permission Testing

  • Create test content
  • Set specific permissions
  • Verify each provider respects permissions correctly

Debugging Authentication Issues

When authentication fails, check these areas first:

// Add detailed logging to your authentication events
options.Events = new OpenIdConnectEvents {
    OnAuthenticationFailed = context => {
        var logger = context.HttpContext.RequestServices
            .GetRequiredService<ILogger<Startup>>();
        logger.LogError(context.Exception, 
            "Authentication failed for {Provider}", 
            context.Scheme.Name);
        
        context.HandleResponse();
        context.Response.Redirect("/error/auth-failed");
        return Task.CompletedTask;
    }
};

Monitor these log sources:

  • ASP.NET Core authentication middleware logs
  • Provider-specific error responses
  • Browser network tab for redirect chains
  • CMS synchronization service logs

Conclusion

Setting up multiple authentication providers in Optimizely CMS 12 gives your users flexibility while maintaining security. By following this guide, you've configured Entra ID, Google, Facebook, and local identity to work together smoothly. The key is understanding how ASP.NET Core authentication middleware chains work and ensuring proper user synchronization with the CMS.

Teams we work with report that offering multiple login options significantly reduces support tickets about password resets and account access. Users appreciate being able to choose their preferred authentication method, especially when it aligns with tools they already use daily.

If you're planning to implement multi-provider authentication in Optimizely CMS 12 and need help with custom requirements like single sign-on across multiple sites or advanced role mapping scenarios, we can guide you through the architecture decisions and implementation details specific to your setup.

Share this article