1761608217
2025-10-27 07:34:00
Optimizely CMS の保護は、ほぼすべてのプロジェクトにおいて常に重要な考慮事項です。組み込みの ASP.NET ID プロバイダーも利用できますが、多くの組織は、ユーザーとロールを一元管理できるクラウドベースの ID ソリューションを好みます。
場合によっては、ローカル ASP.NET ID と外部プロバイダーを調整された方法で組み合わせることで、これら 2 つのアプローチを連携させる必要があります。良いニュースは、.NET の世界では、そのようなワークフローを非常に簡単に構成できることです。ただし、この柔軟性は、慎重に扱わないと不必要な複雑さを招く可能性もあります。
以下は、Optimizely CMS で認証を統合する (およびロールの認可を処理する) ための 1 つの方法に関するステップバイステップの構成ガイドです。
流れ
このシナリオでは、フロントエンド ユーザーは Okta 経由で認証され、フロントエンドがヘッドレスであるため、JWT トークンを使用してサイトにアクセスします。 CMS ユーザーは、MVC アプローチを使用して Okta を通じて認証することもできます。さらに、ASP.NET ID を介したフォールバック ログインは、CMS アクセスに引き続き利用できます。
最初にアプリケーションで使用される認証スキーム名を定義することが重要です。この特定のケースでは、次のことが必要でした。
internal static class AuthScheme
{
internal const string Cms = "CMS-Auth";
internal const string Api = "API-Auth";
internal const string OktaMvc = "Okta-MVC";
internal const string OktaJwt = "Okta-JWT";
internal const string ContentApi = OpenIDConnectOptionsDefaults.AuthenticationScheme;
internal static readonly string Identity = IdentityConstants.ApplicationScheme;
}
2 つの主要な認証スキーマが高レベルで定義されています。
-
CMS: CMS 編集モードでのユーザー アクセスの場合、デフォルトのスキーマとして機能します
-
API: API エンドポイントリクエストの認証用
Okta 認証では 2 つのスキーマが使用されます。
さらに、次のようなものもあります。
-
コンテンツAPI: CMS をコンテンツ プレビュー用の OpenIddict サーバーとして有効にすることで、ヘッドレス アーキテクチャをサポートします
-
身元: デフォルトの ASP.NET データベース ID を表します
このスキーマ構造により、組織が改善され、アプリケーション全体での認証ポリシーの管理が簡素化されます。
services
.AddPolicyScheme(AuthScheme.Cms, null, o =>
{
o.ForwardDefaultSelector = httpContext =>
httpContext.ContainsIdentityAuthCookie()
? AuthScheme.Identity
: AuthScheme.OktaMvc;
})
.AddPolicyScheme(AuthScheme.Api, null, o =>
{
o.ForwardDefaultSelector = httpContext =>
httpContext.IsSwaggerContext()
? AuthScheme.Cms
: AuthScheme.OktaJwt;
});
の ポリシースキームの追加 このメソッドを使用すると、条件付きロジックで使用する認証スキーマを決定できます。デフォルトの CMS スキーマの場合、決定はユーザーが ID Cookie を持っているかどうかによって異なります。存在する場合、サイトでは ASP.NET Identity 認証へのフォールバックが許可されます。それ以外の場合は、Okta チャレンジがトリガーされます。
private static bool ContainsIdentityAuthCookie(this HttpContext context)
{
var config = context.RequestServices.GetRequiredService>();
var identityCookieName = config.Get(AuthScheme.Identity).Cookie.Name;
if (string.IsNullOrWhiteSpace(identityCookieName))
{
return false;
}
return context.Request.Cookies.ContainsKey(identityCookieName);
}
API スキーマは主に Okta JWT トークンを使用し、API エンドポイントの認可を制御します。 [Authorize(AuthScheme.API)] 属性。 Swagger を使用した開発者テストを簡素化するために、条件付きポリシーがこのシナリオを検出し、CMS スキーマにフォールバックします。これにより、有効なブラウザ セッションを介した API アクセスが可能になります。
構成
上記は、さまざまなケースで認証がどのように機能するかを概説しています。残っているのは、各認証スキーマの詳細な構成です。
基本から始めます。最初は ASP.NET Identity です。これには、シンプルな 1 行の構成が必要です。
services.AddCmsAspNetIdentityApplicationUser>();
さらに、ドキュメント化の目的で、Content API 認証フロー構成が含まれていますが、詳細は説明しません。これはヘッドレス アーキテクチャが機能するために必要ですが、アプリケーションのコア ビジネス ロジックには関与しません。
internal static IServiceCollection AddContentApiAuth(this IServiceCollection services, ContentApiAuthOptions? options, bool isDevelopment)
{
services.AddOpenIDConnect(
useDevelopmentCertificate: true,
signingCertificate: null,
encryptionCertificate: null,
createSchema: true,
o =>
{
o.RequireHttps = !isDevelopment;
o.Applications.Add(new OpenIDConnectApplication
{
ClientId = options?.ClientId,
ClientSecret = options?.ClientSecret,
Scopes = { "openid", "offline_access", "profile", "email", "roles", ContentManagementApiOptionsDefaults.Scope },
});
}
);
services.AddOpenIDConnectUI();
return services;
}
internal static IServiceCollection AddContentApiAuth(this IServiceCollection services, ContentApiAuthOptions? options, bool isDevelopment)
{
services.AddOpenIDConnect(
useDevelopmentCertificate: true,
signingCertificate: null,
encryptionCertificate: null,
createSchema: true,
o =>
{
o.RequireHttps = !isDevelopment;
o.Applications.Add(new OpenIDConnectApplication
{
ClientId = options?.ClientId,
ClientSecret = options?.ClientSecret,
Scopes = { "openid", "offline_access", "profile", "email", "roles", ContentManagementApiOptionsDefaults.Scope },
});
}
);
services.AddOpenIDConnectUI();
return services;
} Okta MVC
最後に、焦点は Okta MVC セットアップから始まる Okta 認証の構成に移ります。
internal static AuthenticationBuilder AddOktaAuth(this AuthenticationBuilder authenticationBuilder, OktaAuthOptions? options)
{
ArgumentNullException.ThrowIfNull(options);
authenticationBuilder
.AddOktaMvc(AuthScheme.OktaMvc, new OktaMvcOptions
{
OktaDomain = options.Domain,
AuthorizationServerId = OktaWebOptions.DefaultAuthorizationServerId,
ClientId = options.ClientId,
ClientSecret = options.ClientSecret,
CallbackPath = options.CallbackPath,
PostLogoutRedirectUri = options.SignOutRedirectUrl,
Scope = new Liststring> { "openid", "profile", "email" },
GetClaimsFromUserInfoEndpoint = true,
OpenIdConnectEvents = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context => {
context.Response.Headers.CacheControl = "no-store, no-transform, no-cache";
context.Response.Headers["CDN-Cache-Control"] = "no-store, no-transform, no-cache";
if (context.Response.StatusCode == StatusCodes.Status401Unauthorized) {
context.HandleResponse();
}
if (context.Request.Path.StartsWithSegments($"/{ApiControllerBase.ApiPrefix}"))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.HandleResponse();
}
return Task.CompletedTask;
},
OnAuthenticationFailed = async context => {
context.HandleResponse();
var messageBytes = Encoding.ASCII.GetBytes(context.Exception.Message);
await context.Response.BodyWriter.WriteAsync(messageBytes);
},
OnTokenValidated = context =>
{
EnsureInternalRedirection(context);
return Task.CompletedTask;
},
OnTicketReceived = async context =>
{
OktaClaimsTransformer.Transform(context.Principal);
await SynchronizeRoles(context.HttpContext, context.Principal);
}
}
})
.AddApiAuth(options);
return authenticationBuilder;
}
の Okta.AspNetCore ここでは NuGet パッケージが使用されており、 OktaMvc の追加 拡張メソッドを使用して、認可サーバーの詳細と必要なスコープを構成します。これには、少なくとも以下が含まれます。 オープンID、 プロフィール、 そして 電子メール。さらに、 getClaimsForUserInfoエンドポイント フラグは、 ユーザー情報 エンドポイントは、利用可能なすべてのクレームを取得するために呼び出されます。
特に、いくつかの認証イベントは特定の変更やエッジケースを処理します。の OnRedirectToIdentityProvider イベントはキャッシュとリダイレクト ループを防止し、Swagger コンテキストでログイン ページにリダイレクトする代わりに 401 ステータス コードが返されるようにします。
他のイベント ハンドラーは、認証が成功したときに関連するメッセージを返し、内部リダイレクトを回避することでエラーを管理します。
private static void EnsureInternalRedirection(TokenValidatedContext context)
{
if (string.IsNullOrEmpty(context.Properties?.RedirectUri))
{
return;
}
try
{
var redirectUri = new Uri(context.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
if (redirectUri.IsAbsoluteUri)
{
context.Properties.RedirectUri = redirectUri.PathAndQuery;
}
}
catch (UriFormatException)
{
context.Properties.RedirectUri = "/";
}
}
トークンが完全に受信されると、最後のイベントがトリガーされます。これは、特定のビジネス要件に基づいて Okta からのクレームを変更するために使用されます。さらに重要なのは、このステップです 必要なロールを Optimizely CMS と同期しますこれは、Okta 経由で認証されたユーザーが CMS インターフェイスへの適切なアクセス権を持つために重要です。
private static async Task SynchronizeRoles(HttpContext httpContext, ClaimsPrincipal? principal)
{
if (principal?.Identity is not ClaimsIdentity claimsIdentity)
{
return;
}
var optiClaims = new List
{
new(ClaimTypes.Role, "WebAdmins", ClaimValueTypes.String),
};
claimsIdentity.AddClaims(optiClaims);
var syncService = httpContext
.RequestServices
.GetRequiredService();
await userSyncService.SynchronizeAsync(claimsIdentity);
}
理想的には、すべてのロールが Okta から直接提供されることになります。ただし、これが常に実現できるとは限りません。そのような場合、ここでカスタム ソリューションを導入できます。の userSyncService の例です ISynchronizingUserService から EPiServer.セキュリティ 名前空間。その中心的な責任は、新しいレコードを tbl同期ユーザー、 tblSyncedUserRelations、 そして tblSyncedUserRole データベース内のテーブル。ユーザーを特定の Optimizely ロールにマップします。
Okta MVC には、 OktaMvc の追加 方法。この構成は、サービスが登録された後の構成後のステップで実行する必要があります。
internal static IServiceCollection ConfigureOktaTokenValidation(this IServiceCollection services, OktaAuthOptions? options)
{
ArgumentNullException.ThrowIfNull(options);
return services.PostConfigureAll(o =>
{
var validationParameters = GetTokenValidationParameters(AuthScheme.OktaMvc);
validationParameters.ValidIssuer = options.Domain;
validationParameters.ValidAudience = options.ClientId;
o.TokenValidationParameters = validationParameters;
});
}
必須パラメータを設定することでトークンを検証する方法を定義します。また、許容範囲ウィンドウ (クロックスキュー) これにより、複数のサーバー間のわずかな時刻の差異にもかかわらず、トークンが短期間有効であると見なされます。
private static TokenValidationParameters GetTokenValidationParameters(string authenticationType) =>
new()
{
RoleClaimType = SiteClaimTypes.Role,
NameClaimType = SiteClaimTypes.Username,
AuthenticationType = authenticationType,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
ValidateIssuerSigningKey = true
};
オクタ JWT
Okta MVC が構成されたら、次のステップは Okta JWT をセットアップすることです。セットアップは非常に似ていますが、発行者の URL の構築など、いくつかの小さな違いがあります。
の設定 地図インバウンドクレーム false にフラグを設定すると、クレームの自動マッピングが無効になり、クレームの変換方法を完全に制御できるようになります。これにより、カスタム ロジックで MVC と JWT の両方の認証フローのクレームが一貫して調整されるようになります。手動マッピングは、既定の規則に依存するのではなく、ソース クレームに対して実行されます。
ここでは、MVC と同じロジックを使用してユーザー ロールを同期するためのイベント ハンドラーが 1 つだけ指定されています。主な違いは、 OnTokenValidated の代わりに OnTokenReceived。この選択は、これらのイベントを組み合わせることで完全なクレーム セットが提供されることが判明したテストに基づいています。使用する OnTokenValidated MVC スキーマ内に単独で存在した場合、クレームが欠落してしまいました。 OnTokenReceived。
private static AuthenticationBuilder AddApiAuth(this AuthenticationBuilder authenticationBuilder, OktaAuthOptions options) =>
authenticationBuilder
.AddJwtBearer(AuthScheme.OktaJwt, o =>
{
o.Authority = UrlHelper.CreateIssuerUrl(options.Domain, AuthorizationServerId);
o.Audience = options.ApiAudience;
o.RequireHttpsMetadata = true;
o.TokenValidationParameters = GetTokenValidationParameters(AuthScheme.OktaJwt);
o.MapInboundClaims = false;
o.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
OktaClaimsTransformer.Transform(context.Principal);
await SynchronizeRoles(context.HttpContext, context.Principal);
}
};
});
すべてのパーツを接着する
すべてのスキーマを構成したら、最後のステップは完全な認証システムを組み立てることです。
private static IServiceCollection AddMixedAuth(this IServiceCollection services, AuthOptions? options, bool isDevelopment)
{
ArgumentNullException.ThrowIfNull(options);
services
.AddCmsAspNetIdentityApplicationUser>()
.ConfigureApplicationCookie(o =>
{
o.Cookie.HttpOnly = true;
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddSession(o =>
{
o.Cookie.HttpOnly = true;
o.Cookie.IsEssential = true;
})
.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = AuthScheme.Cms;
o.DefaultChallengeScheme = AuthScheme.Cms;
})
.AddOktaAuth(options.Okta)
.AddPolicyScheme(AuthScheme.Cms, null, o =>
{
o.ForwardDefaultSelector = httpContext =>
httpContext.ContainsIdentityAuthCookie()
? AuthScheme.Identity
: AuthScheme.OktaMvc;
})
.AddPolicyScheme(AuthScheme.Api, null, o =>
{
o.ForwardDefaultSelector = httpContext =>
httpContext.IsSwaggerContext()
? AuthScheme.Cms
: AuthScheme.OktaJwt;
});
services.ConfigureOktaTokenValidation(options.Okta);
services.AddContentApiAuth(options?.ContentApi, isDevelopment);
return services;
}
の 混合認証の追加 このメソッドはすべての認証設定を統合します。 Cookie とセッションの処理を設定し、ポリシーを定義し、デフォルトのスキーマを確立します。この方法は、セットアップのすべての部分が接続される単一の集中ポイントとして機能します。これは、スタートアップ ファイルなどの上位レベルで便利に使用でき、完全な混合認証構成をアクティブ化できます。
services.AddMixedAuth(options, isDevelopment);
追加の役立つコードは、 マップログアウトルート 拡張メソッド。 設定する スタートアップファイルのメソッド。その役割は、現在のスキーマ コンテキストを検出し、適切なサインアウト手順を実行することです。
internal static IApplicationBuilder MapLogoutRoute(this IApplicationBuilder app, AuthOptions? options)
{
app.MapWhen(context => context.Request.Path.StartsWithSegments("/Util/Logout"), appBuilder =>
{
appBuilder.Run(async httpContext =>
{
if (httpContext.ContainsIdentityAuthCookie())
{
var signInManager = httpContext.RequestServices.GetRequiredService();
await signInManager.SignOutAsync();
}
else
{
await httpContext.SignOutAsync();
}
httpContext.Response.Redirect(options?.Okta?.SignOutRedirectUrl ?? "/", false);
});
});
return app;
}
最後の言葉
上で説明したアプローチは、私が取り組んだプロジェクトの 1 つで、かなり複雑で興味深い認証と認可のセットアップを明確に構成するのに役立ちました。これは、何時間もかけて調査し、ドキュメントを読み、適切に構造化された構成セットアップを設計した結果です。この構造は拡張が簡単で、スムーズに有効または無効にできるため、現実のシナリオに実用的です。
#Optimizely #CMS #混合認証 #Okta #ASP.NET