1761198808
2025-10-22 20:03:00
最近の Web サイトでは、多くの場合、ユーザーが自分のアカウントでサインインできるようにする必要があります。 法人アカウント(エントラID)、 彼らの ソーシャル アイデンティティ (Google、Facebook)、または単純な メールアドレス/パスワード 内部ユーザー向け。
上に構築している場合 CMS 12 の最適化、サポートできます それらすべてを一度に — 組み込みの CMS ログイン システムを壊すことなく。
この投稿では、次の方法を説明します。
- 保つ CMS 12 ローカル ID を最適化する (メールアドレス/パスワード)
/util/login - 追加 Entra ID (Azure AD) で
/login - 追加 グーグル で
/login/google - 追加 フェイスブック で
/login/facebook - 使用 共有クッキー 1 つ すべての外部プロバイダー向け
- 自動的に ユーザー/ロールを同期する オプティマイズリーへ
インストールする 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 パッケージがあります。上記の最初のパッケージは、CMS UI で使用される ID ヘルパーを組み込みます。
追加するファイル
// 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"
}
}
}
フェイスブック: Facebook アプリの「有効な OAuth リダイレクト URI」がサイトの URI と一致していることを確認してください。 https://your-host/signin-facebook。
グーグル: 許可する https://your-host/signin-google。
入力: リダイレクトを設定します https://your-host/signin-oidc。
仕組み
- /util/ログイン 用途 ローカルアイデンティティを最適化する (電子メール/パスワード) — 変更なし。
- /ログイン、 /ログイン/グーグル、 /ログイン/フェイスブック へのルート 入力、 グーグル、 フェイスブック それぞれ。
- すべての外部プロバイダーがサインインします。 同じクッキー (
azure-cookie)。 - Cookie サインイン時に、
ISynchronizingUserService.SynchronizeAsync(...)これにより、ユーザー/ロールが CMS に表示されます。 - を追加します
"auth_provider"これにより、誰がユーザーをログインさせたかを知ることができます。
#複数の認証プロバイダを #Optimizely #CMS #サイトに追加する方法 #Entra #IDGoogleFacebookおよび #Local #Identity #PowerBuilder