Как добавить несколько поставщиков аутентификации на сайт Optimizely CMS 12 (Entra ID, Google, Facebook и локальная идентификация) – PowerBuilder

Современные веб-сайты часто должны позволять пользователям входить в систему с помощью своих корпоративный аккаунт (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" утверждают, чтобы вы могли узнать, кто вошел в систему.
Read more:  Обвинение против двух россиян в Польше. Они должны были работать на Кремль.

Ещё по этой теме

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.