/
softailor
/
gridient
Обзор
Документация
Войти
/
softailor
/
gridient
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
backend/Gridient.Host.PostgreSQL/Configuring/AuthenticationConfiguration.cs
317 строк
22 KB
Jenkins
Auto add
18 фев 2026, 10:19
18 фев 2026, 10:19
1e6c4f8
Код
Авторство
О чём код?
using Gridient.Core.Services.Interfaces; using Gridient.Infrastructure.Authentication.Local.Options; using Gridient.Infrastructure.Authentication.Oidc.Options; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; using System.Security.Claims; namespace Gridient.Configuring { public static class AuthenticationConfiguration { public static void AddGridientAuthentication( this IServiceCollection services, IConfiguration configuration) { // Настройки для JWT и OIDC var jwtOptions = configuration.GetSection("Jwt").Get<JwtOptions>(); var oidcOptions = configuration.GetSection("Oidc").Get<OidcOptions>() ?? new OidcOptions(); var claimMappings = configuration.GetSection("Auth:ClaimMappings") .Get<ClaimMappingsOptions>() ?? new ClaimMappingsOptions(); // ============================================================================ // ГИБРИДНАЯ СХЕМА АУТЕНТИФИКАЦИИ // ============================================================================ // Поддерживает два режима работы одновременно: // 1. Local (JWT) - для мобильных приложений и API-клиентов // - Пользователь вводит логин/пароль // - Бэкенд выдаёт JWT токен // - Клиент отправляет токен в заголовке Authorization: Bearer <token> // 2. OIDC (Cookie) - для браузерных приложений // - Пользователь входит через внешний провайдер (PocketID) // - Сессия хранится в cookie .AspNetCore.Cookies // - Браузер автоматически отправляет cookie при каждом запросе // // Гибридная схема автоматически выбирает нужную схему на основе наличия // заголовка Authorization в запросе. // ============================================================================ services .AddAuthentication(options => { options.DefaultScheme = "GridientHybrid"; options.DefaultChallengeScheme = "GridientHybrid"; }) .AddPolicyScheme("GridientHybrid", "Gridient hybrid auth", options => { // Логика выбора схемы аутентификации: // - Если запрос содержит заголовок Authorization → используем JWT схему // (это означает, что клиент отправляет JWT токен, используется для Local-пользователей) // - Если заголовка Authorization нет → используем Cookie схему // (это означает браузерный запрос, используется для OIDC-пользователей) options.ForwardDefaultSelector = context => { return context.Request.Headers.ContainsKey("Authorization") ? JwtBearerDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme; }; }) // ============================================================================ // COOKIE СХЕМА АУТЕНТИФИКАЦИИ // ============================================================================ // Используется для хранения сессии пользователя после OIDC-аутентификации. // // Почему нужна Cookie схема вместе с OIDC? // - OIDC схема обрабатывает только процесс входа (редирект на провайдера, // получение токенов, валидация). Она НЕ хранит сессию. // - После успешной OIDC-аутентификации нужно сохранить информацию о пользователе // для последующих запросов. Для этого используется Cookie схема. // // Как это работает: // 1. OIDC схема обрабатывает вход и получает claims из токена // 2. В OnTokenValidated добавляются роли пользователя из БД // 3. OIDC схема передаёт управление Cookie схеме (через SignInScheme) // 4. Cookie схема создаёт cookie .AspNetCore.Cookies с сериализованным ClaimsPrincipal // 5. При последующих запросах Cookie схема автоматически восстанавливает // пользователя из cookie и устанавливает HttpContext.User // // Альтернативы (не используются в этом проекте): // - Можно использовать только OIDC с токенами (без Cookie), но тогда нужно // вручную управлять токенами на фронтенде и отправлять их в Authorization // - Для браузерных приложений Cookie схема - оптимальный выбор // ============================================================================ .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => { options.Events = new CookieAuthenticationEvents { OnRedirectToLogin = context => { if (IsApiRequest(context.Request)) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; } context.Response.Redirect(context.RedirectUri); return Task.CompletedTask; }, OnRedirectToAccessDenied = context => { if (IsApiRequest(context.Request)) { context.Response.StatusCode = StatusCodes.Status403Forbidden; return Task.CompletedTask; } context.Response.Redirect(context.RedirectUri); return Task.CompletedTask; } }; }) // ============================================================================ // JWT СХЕМА АУТЕНТИФИКАЦИИ // ============================================================================ // Используется для валидации JWT токенов, выданных при Local-аутентификации // (логин/пароль через /api/Auth/login). // // Когда используется: // - Клиент отправляет запрос с заголовком Authorization: Bearer <token> // - Гибридная схема выбирает JWT схему для валидации токена // - Подходит для мобильных приложений и API-клиентов // ============================================================================ .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidIssuer = jwtOptions?.Issuer, ValidAudience = jwtOptions?.Audience, ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey( System.Text.Encoding.UTF8.GetBytes(jwtOptions?.Secret ?? string.Empty)) }; }) // ============================================================================ // OIDC СХЕМА АУТЕНТИФИКАЦИИ // ============================================================================ // Обрабатывает процесс входа через внешний OIDC-провайдер (PocketID). // // Что делает OIDC схема: // 1. Редиректит пользователя на провайдера для авторизации // 2. Получает authorization code от провайдера // 3. Обменивает code на id_token и access_token // 4. Валидирует токены // 5. Извлекает claims из токена // 6. Вызывает событие OnTokenValidated // // ВАЖНО: OIDC схема НЕ хранит сессию сама по себе! // Она только обрабатывает процесс входа. Для хранения сессии нужна // дополнительная схема (в нашем случае - Cookie схема). // ============================================================================ .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { options.Authority = oidcOptions?.Authority; options.ClientId = oidcOptions?.ClientId; options.ClientSecret = oidcOptions?.ClientSecret; options.ResponseType = oidcOptions?.ResponseType ?? "code"; options.Scope.Clear(); foreach (var scope in (oidcOptions?.Scope ?? "openid profile email").Split(' ')) { options.Scope.Add(scope); } options.GetClaimsFromUserInfoEndpoint = oidcOptions?.GetClaimsFromUserInfoEndpoint ?? true; options.SaveTokens = oidcOptions?.SaveTokens ?? true; // ======================================================================== // КРИТИЧЕСКИ ВАЖНАЯ НАСТРОЙКА: SignInScheme // ======================================================================== // Указывает, какая схема будет использоваться для сохранения сессии // после успешной OIDC-аутентификации. // // Как это работает: // 1. OIDC схема обрабатывает вход и получает claims из токена // 2. В OnTokenValidated добавляются роли пользователя из БД // 3. OIDC схема передаёт управление Cookie схеме (через SignInScheme) // 4. Cookie схема создаёт cookie .AspNetCore.Cookies с ClaimsPrincipal // 5. При последующих запросах Cookie схема автоматически восстанавливает // пользователя из cookie // // Альтернативы (не используются): // - SignInScheme = null - токены сохраняются в свойствах аутентификации, // но нужно вручную управлять ими на фронтенде // - SignInScheme = JwtBearerDefaults.AuthenticationScheme - можно использовать // JWT схему, но это требует дополнительной логики для создания JWT из OIDC токенов // ======================================================================== options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.TokenValidationParameters ??= new Microsoft.IdentityModel.Tokens.TokenValidationParameters(); options.TokenValidationParameters.ValidIssuer = oidcOptions?.Authority?.TrimEnd('/'); options.TokenValidationParameters.ValidateIssuer = true; options.CorrelationCookie.SameSite = SameSiteMode.Lax; options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.NonceCookie.SameSite = SameSiteMode.Lax; options.NonceCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Маппинг claims options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, claimMappings.UserIdClaim); options.ClaimActions.MapJsonKey(ClaimTypes.Name, claimMappings.LoginClaim); options.ClaimActions.MapJsonKey(ClaimTypes.Email, claimMappings.EmailClaim); options.ClaimActions.MapJsonKey("display_name", claimMappings.DisplayNameClaim); // Маппинг ролей foreach (var roleClaimType in claimMappings.RoleClaims) { options.ClaimActions.MapJsonKey(ClaimTypes.Role, roleClaimType); } // События для автоматического создания пользователей options.Events ??= new OpenIdConnectEvents(); options.Events.OnRedirectToIdentityProvider = context => { if (IsApiRequest(context.Request)) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.HandleResponse(); } return Task.CompletedTask; }; // Обработка logout: явно указываем end_session_endpoint, если discovery не сработал options.Events.OnRedirectToIdentityProviderForSignOut = context => { // Если end_session_endpoint не найден автоматически, устанавливаем его вручную if (string.IsNullOrEmpty(context.ProtocolMessage.IssuerAddress) && !string.IsNullOrEmpty(oidcOptions?.Authority)) { var authority = oidcOptions.Authority.TrimEnd('/'); // Пытаемся использовать стандартный путь для end_session_endpoint // Если провайдер использует другой путь, его можно указать в конфигурации context.ProtocolMessage.IssuerAddress = $"{authority}/api/oidc/end-session"; } // Устанавливаем post_logout_redirect_uri для возврата после logout if (!string.IsNullOrEmpty(context.Properties?.RedirectUri)) { context.ProtocolMessage.PostLogoutRedirectUri = context.Properties.RedirectUri; } return Task.CompletedTask; }; // ======================================================================== // СОБЫТИЕ: OnTokenValidated // ======================================================================== // Вызывается после успешной валидации OIDC токенов, но ДО создания cookie. // Здесь можно: // - Создать/найти пользователя в БД на основе claims из токена // - Добавить дополнительные claims (например, роли из БД) // - Выполнить любую другую логику перед сохранением сессии // // ВАЖНО: Изменения в context.Principal будут сохранены в cookie, // потому что после этого события OIDC схема передаст управление // Cookie схеме (через SignInScheme). // ======================================================================== options.Events.OnTokenValidated = async context => { var userProvisioningService = context.HttpContext.RequestServices .GetRequiredService<IUserProvisioningService>(); var userService = context.HttpContext.RequestServices .GetRequiredService<IUserService>(); var claims = context.Principal?.Claims ?? Enumerable.Empty<Claim>(); // Создаём или находим пользователя в БД на основе claims из OIDC токена var user = await userProvisioningService.GetOrCreateUserFromClaimsAsync(claims, CancellationToken.None); // Добавляем роли пользователя из БД в claims для cookie-сессии // Эти роли будут сохранены в cookie и доступны при всех последующих запросах // ПРИМЕЧАНИЕ: При использовании ABAC проверка прав доступа выполняется через IPolicyEvaluator, // поэтому добавление ролей в claims закомментировано // Закомментировано: добавление групп в клаймы (не нужно при использовании ABAC) // if (user != null && context.Principal?.Identity is ClaimsIdentity identity) // { // // Получаем полную информацию о пользователе с группами // var fullUser = await userService.GetByLoginAsync(user.Login, CancellationToken.None); // if (fullUser == null) // { // // Пользователь не найден в Core, роли не добавляются // // Аутентификация продолжается, но без ролей из Core // return; // } // if (fullUser.Groups != null) // { // // Добавляем роли из групп пользователя (только IsDefault группы, как в LocalAuthService) // foreach (var group in fullUser.Groups.Where(g => g.IsDefault)) // { // if (!identity.HasClaim(ClaimTypes.Role, group.Caption)) // { // identity.AddClaim(new Claim(ClaimTypes.Role, group.Caption)); // } // } // } // } // После завершения этого события OIDC схема передаст управление // Cookie схеме, которая создаст cookie с обновлённым ClaimsPrincipal }; }); } private static bool IsApiRequest(HttpRequest? request) { if (request == null) { return false; } if (request.Path.StartsWithSegments("/api/auth/oidc", StringComparison.OrdinalIgnoreCase)) { return false; } return request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase); } } }