
How to Add Multiple Authentication Providers to Your Optimizely CMS 12 Site
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.
