Современные веб-сайты часто должны позволять пользователям входить в систему с помощью своих корпоративный аккаунт (Entra ID)их социальная идентичность (Google, Facebook)или простой электронная почта/пароль для внутренних пользователей.
Если вы строите Оптимизация CMS 12ты можешь поддержать все они одновременно — без нарушения встроенной системы входа в CMS.
В этом посте мы рассмотрим, как:
- Держать Оптимизировать локальную идентификацию CMS 12 (электронная почта/пароль) по адресу
/util/login - Добавлять Идентификатор входа (Azure AD) в
/login - Добавлять Google в
/login/google - Добавлять Фейсбук в
/login/facebook - Использовать один общий файл cookie для всех внешних поставщиков
- Автоматически синхронизировать пользователей/роли в Оптимизировать
Пакеты NuGet для установки
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
В вашем проекте уже есть пакеты Optimizely CMS 12; первый пакет выше содержит помощники Identity, используемые пользовательским интерфейсом CMS.
Файлы для добавления
// File: Infrastructure/Security/AuthenticationExtensions.cs
using System.Security.Claims;
using System.Text;
using EPiServer.Cms.UI.AspNetIdentity;
using EPiServer.ServiceLocation;
using EPiServer.Shell.Security;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Facebook;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
namespace YourNamespace.Infrastructure.Security
{
public static class AuthenticationExtensions
{
private const string ExternalAppCookie = "azure-cookie"; // shared external cookie
///
/// Keep Optimizely CMS local Identity (email/password). Exposes /util/login.
///
public static IServiceCollection UseOptimizelyCmsIdentity(
this IServiceCollection services,
IConfiguration configuration) where TUser : SiteUser, new()
{
services.AddCmsAspNetIdentity(o =>
{
var conn = configuration.GetConnectionString("EPiServerDB");
if (!string.IsNullOrWhiteSpace(conn))
{
o.ConnectionStringOptions = new ConnectionStringOptions
{
Name = "EPiServerDB",
ConnectionString = conn
};
}
});
return services;
}
///
/// Adds Entra ID (Azure AD) via OpenIdConnect. Uses the shared external cookie.
/// Required config keys:
/// Authentication:AzureClientID, Authentication:AzureClientSecret, Authentication:azureAuthority, Authentication:CallbackPath
///
public static IServiceCollection UseEntraIdForCms(this IServiceCollection services, IConfiguration configuration)
{
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; // helpful during setup
var clientId = configuration["Authentication:AzureClientID"];
var clientSecret = configuration["Authentication:AzureClientSecret"];
var azureAuthority = configuration["Authentication:azureAuthority"]; // e.g. https://login.microsoftonline.com//v2.0
var callbackPath = configuration["Authentication:CallbackPath"] ?? "/signin-oidc";
services.AddAuthentication()
.AddCookie(ExternalAppCookie, options =>
{
// Any external login that signs into this cookie will trigger sync
options.Events = new CookieAuthenticationEvents
{
OnSignedIn = async ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity id)
{
var sync = ctx.HttpContext.RequestServices.GetRequiredService();
await sync.SynchronizeAsync(id);
}
}
};
})
.AddOpenIdConnect("azure", options =>
{
options.SignInScheme = ExternalAppCookie;
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
options.ClientId = clientId;
options.ClientSecret = clientSecret;
options.Authority = azureAuthority;
options.CallbackPath = new PathString(callbackPath);
// Ensure standard profile/email claims flow in for synchronization
options.Scope.Clear();
options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
options.Scope.Add(OpenIdConnectScope.Email);
options.MapInboundClaims = false; // we’ll control mappings
options.GetClaimsFromUserInfoEndpoint = true; // pull extra claims if needed
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles",
NameClaimType = "preferred_username",
ValidateIssuer = false
};
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = ctx =>
{
if (ctx.Response.StatusCode == StatusCodes.Status401Unauthorized)
{
ctx.HandleResponse();
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(context.Exception.Message));
return Task.CompletedTask;
},
OnTokenValidated = ctx =>
{
// Safe redirect normalization (avoid relative-URI exception)
var redirect = ctx.Properties?.RedirectUri;
if (!string.IsNullOrEmpty(redirect) &&
Uri.TryCreate(redirect, UriKind.RelativeOrAbsolute, out var uri) &&
uri.IsAbsoluteUri)
{
ctx.Properties.RedirectUri = uri.PathAndQuery;
}
// Tag provider
if (ctx.Principal?.Identity is ClaimsIdentity id)
{
id.AddClaim(new Claim("auth_provider", "entra"));
}
// Fire-and-forget sync (cookie event will also run on next req)
ServiceLocator.Current
.GetInstance()
.SynchronizeAsync(ctx.Principal?.Identity as ClaimsIdentity);
return Task.CompletedTask;
}
};
});
return services;
}
///
/// Adds Google OAuth. Signs into the shared external cookie.
///
public static IServiceCollection UseGoogleForCms(this IServiceCollection services, IConfiguration configuration)
{
var clientId = configuration["Authentication:Google:ClientId"];
var clientSecret = configuration["Authentication:Google:ClientSecret"];
services.AddAuthentication()
.AddGoogle("google", options =>
{
options.SignInScheme = ExternalAppCookie;
options.ClientId = clientId;
options.ClientSecret = clientSecret;
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
options.Events = new OAuth.OAuthEvents
{
OnCreatingTicket = ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity id)
id.AddClaim(new Claim("auth_provider", "google"));
return Task.CompletedTask;
}
};
});
return services;
}
///
/// Adds Facebook OAuth. Signs into the shared external cookie.
/// Requires Authentication:Facebook:AppId and :AppSecret.
///
public static IServiceCollection UseFacebookForCms(this IServiceCollection services, IConfiguration configuration)
{
var appId = configuration["Authentication:Facebook:AppId"];
var appSecret = configuration["Authentication:Facebook:AppSecret"];
services.AddAuthentication()
.AddFacebook("facebook", options =>
{
options.SignInScheme = ExternalAppCookie;
options.AppId = appId;
options.AppSecret = appSecret;
options.Scope.Clear();
options.Scope.Add("email");
options.Scope.Add("public_profile");
options.Fields.Add("email");
options.Fields.Add("first_name");
options.Fields.Add("last_name");
options.Fields.Add("name");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "first_name");
options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "last_name");
options.Events = new OAuth.OAuthEvents
{
OnCreatingTicket = ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity id)
id.AddClaim(new Claim("auth_provider", "facebook"));
return Task.CompletedTask;
}
};
});
return services;
}
}
}
// File: Infrastructure/Security/MultiAuthExtensions.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
namespace YourNamespace.Infrastructure.Security
{
public static class MultiAuthExtensions
{
public static IServiceCollection UseMultiAuthGateway(this IServiceCollection services)
{
const string AppCookie = "azure-cookie";
const string IdentityCookie = IdentityConstants.ApplicationScheme;
services.AddAuthentication()
// Authenticate: choose whichever cookie is present
.AddPolicyScheme("smart-auth", "Smart Auth", options =>
{
options.ForwardDefaultSelector = ctx =>
{
var cookies = ctx.Request.Cookies;
if (cookies.ContainsKey(".AspNetCore." + AppCookie)) return AppCookie;
if (cookies.ContainsKey(".AspNetCore." + IdentityCookie)) return IdentityCookie;
return AppCookie; // default
};
})
// Challenge: route by path
.AddPolicyScheme("smart-challenge", "Smart Challenge", options =>
{
options.ForwardDefaultSelector = ctx =>
{
var path = (ctx.Request.Path.Value ?? string.Empty).ToLowerInvariant();
if (path.StartsWith("/login/google")) return "google";
if (path.StartsWith("/login/facebook")) return "facebook";
if (path.StartsWith("/login")) return "azure"; // Entra ID
if (path.StartsWith("/util/login")) return IdentityCookie; // Optimizely local
return "azure"; // default challenge
};
});
// Make our policy schemes the defaults
services.PostConfigure(o =>
{
o.DefaultScheme = "smart-auth";
o.DefaultAuthenticateScheme = "smart-auth";
o.DefaultChallengeScheme = "smart-challenge";
o.DefaultSignInScheme = AppCookie; // shared external cookie
});
return services;
}
}
}
// File: Controllers/LoginController.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
namespace YourNamespace.Controllers
{
[Route("login")]
public class LoginController : Controller
{
[HttpGet("")]
public IActionResult Microsoft([FromQuery] string returnUrl = "/")
=> Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, "azure");
[HttpGet("google")]
public IActionResult Google([FromQuery] string returnUrl = "/")
=> Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, "google");
[HttpGet("facebook")]
public IActionResult Facebook([FromQuery] string returnUrl = "/")
=> Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, "facebook");
public async Task Logout()
{
var authProvider = User.FindFirst("auth-provider")?.Value;
if (string.IsNullOrEmpty(authProvider))
{
await signInManager.SignOutAsync();
}
else
{
await ControllerContext.HttpContext.SignOutAsync("azure-cookie");
HttpContext.Response.Cookies.Delete($".AspNetCore.{"azure-cookie"}");
}
return Redirect(urlResolver.GetUrl(PageContext.ContentLink, PageContext.LanguageID) ?? "/");
}
}
}
В ConfigureServices:
services.UseOptimizelyCmsIdentity(Configuration);
services.UseEntraIdForCms(Configuration);
services.UseGoogleForCms(Configuration);
services.UseFacebookForCms(Configuration);
services.UseMultiAuthGateway();
В 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"
}
}
}
Фейсбук: убедитесь, что ваш «Действительный URI перенаправления OAuth» в приложении Facebook соответствует идентификатору вашего сайта. https://your-host/signin-facebook.
Google: позволять https://your-host/signin-google.
Входить: установить перенаправление на https://your-host/signin-oidc.
Как это работает
- /util/логин использует Оптимизация местной идентичности (e-mail/пароль) — без изменений.
- /авторизоваться, /логин/гугл, /логин/фейсбук маршрут к Входить, Google, Фейсбук соответственно.
- Все внешние поставщики входят в систему то же печенье (
azure-cookie). - При входе в систему с использованием файлов cookie мы вызываем
ISynchronizingUserService.SynchronizeAsync(...)поэтому пользователи/роли появляются в CMS. - Мы добавляем
"auth_provider"утверждают, чтобы вы могли узнать, кто вошел в систему.
Ещё по этой теме

