Смешанная аутентификация Optimizely CMS — Okta + ASP.NET Identity

Обеспечение безопасности Optimizely CMS всегда было ключевым моментом практически в каждом проекте. Хотя доступен встроенный поставщик удостоверений ASP.NET, многие организации предпочитают облачные решения для удостоверений, которые позволяют централизованно управлять пользователями и ролями.

В некоторых случаях эти два подхода должны работать вместе — скоординировано объединяя локальную идентификацию ASP.NET с внешним поставщиком. Хорошей новостью является то, что в мире .NET такие рабочие процессы можно настроить довольно легко. Однако эта гибкость также может привести к ненужным сложностям, если с ней не обращаться осторожно.

Ниже приведено пошаговое руководство по настройке одного из способов интеграции аутентификации (и обработки авторизации для ролей) в Optimizely CMS.

Поток

В этом сценарии пользователи внешнего интерфейса аутентифицируются через Okta и получают доступ к сайту с использованием токенов JWT, поскольку внешний интерфейс является «безголовым». Пользователи CMS также проходят аутентификацию через Okta, используя подход MVC. Кроме того, резервный вход в систему через ASP.NET Identity остается доступным для доступа к 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;
}

На высоком уровне определены две основные схемы аутентификации:

  • система управления контентом: для доступа пользователей в режиме редактирования CMS служит схемой по умолчанию.

  • API: для аутентификации запросов конечной точки API

Аутентификация Okta использует две схемы:

Дополнительно имеются:

  • API контента: поддерживает безголовую архитектуру, включив CMS в качестве сервера OpenIddict для предварительного просмотра контента.

  • Личность: представляет идентификатор базы данных ASP.NET по умолчанию.

Эта структура схемы улучшает организацию и упрощает управление политиками аутентификации в приложении.

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 по умолчанию решение зависит от того, есть ли у пользователя идентификационный файл cookie. Если присутствует, сайт позволяет вернуться к аутентификации личности ASP.NET; в противном случае запускается вызов 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 через действительный сеанс браузера.

Read more:  Победа в тихой производительности: запланированное задание по обслуживанию индекса SQL в Optimizely

Конфигурация

Выше описано, как аутентификация должна функционировать в различных случаях. Остаётся подробная конфигурация для каждой схемы аутентификации.

Начнем с основ. Первым из них является ASP.NET Identity, который требует простой однострочной конфигурации.

services.AddCmsAspNetIdentity<ApplicationUser>();

Кроме того, в целях документации включена конфигурация потока аутентификации 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;
    }

Окта МВЦ

Наконец, фокус смещается на настройку аутентификации Okta, начиная с настройки Okta MVC.

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 List<string> { "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;
    }

Окта.AspNetCore Здесь используется пакет NuGet, предоставляющий АддОктаМвк метод расширения для настройки сведений о сервере авторизации вместе с необходимыми областями, которые как минимум включают в себя открытый идентификатор, профильи электронная почта. Кроме того, getClaimsForUserInfoEndpoint флаг гарантирует, что информация о пользователе Конечная точка вызывается для получения всех доступных утверждений.

Примечательно, что некоторые события аутентификации обрабатывают определенные модификации и крайние случаи. ОнРедиректтоидентитипровидер Событие предотвращает циклы кэширования и перенаправления и гарантирует возврат кода состояния 401 вместо перенаправления на страницу входа в контекст Swagger.

Другие обработчики событий управляют ошибками, возвращая соответствующие сообщения и избегая внутренних перенаправлений при успешной аутентификации.

    
    
    
    
    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);
    }

В идеале все роли должны предоставляться напрямую из Окты. Однако это не всегда осуществимо. В таких случаях здесь можно предложить индивидуальное решение. пользовательсинксервице является примером ISynchronizingUserService от EPiServer.Security пространство имен. Его основная обязанность — добавлять новые записи в tblSyncchedUser, tblSyncedUserRelationsи tblSyncchedUserRole таблицы в базе данных, которые сопоставляют пользователей с конкретными ролями Optimizely.

Read more:  Полный выпуск: вторник, 4 ноября 2025 г.

Для Okta MVC требуется небольшая дополнительная настройка, которая не обрабатывается в АддОктаМвк метод. Эту настройку необходимо выполнить на этапе после настройки после регистрации служб.

    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. Ключевое отличие заключается в использовании 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
            .AddCmsAspNetIdentity<ApplicationUser>()
            .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;
    }

Заключительные слова

Описанный выше подход помог наглядно настроить достаточно сложную и интересную настройку аутентификации и авторизации в одном из проектов, над которыми я работал. Это результат многих часов исследований, чтения документации и разработки хорошо структурированной конфигурации. Эту структуру легко расширить, и ее можно плавно включать и отключать, что делает ее практичной для реальных сценариев.

Read more:  ЕС отправляет больше генераторов в Украину после отключения отопления

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

Leave a Comment

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