/
softailor
/
gridient
Обзор
Документация
Войти
/
softailor
/
gridient
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
backend/Gridient.Host.PostgreSQL/Configuring/OidcAuthenticationConfiguration.cs
280 строк
19 KB
Jenkins
Auto add
18 фев 2026, 10:19
18 фев 2026, 10:19
1e6c4f8
Код
Авторство
О чём код?
using Gridient.Core.Services.Interfaces; using Gridient.Infrastructure.Authentication.Oidc.Options; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; using System.Security.Claims; namespace Gridient.Configuring { /// <summary> /// Конфигурация аутентификации только для OIDC режима (Cookie). /// Используется для браузерных приложений с внешним провайдером (PocketID). /// </summary> public static class OidcAuthenticationConfiguration { /// <summary> /// Настраивает аутентификацию только через OIDC (Cookie режим). /// Пользователи входят через внешний провайдер (PocketID), /// сессия хранится в cookie .AspNetCore.Cookies /// </summary> public static void AddGridientOidcAuthentication( this IServiceCollection services, IConfiguration configuration) { // Настройки для OIDC var oidcOptions = configuration.GetSection("Oidc").Get<OidcOptions>() ?? new OidcOptions(); var claimMappings = configuration.GetSection("Auth:ClaimMappings") .Get<ClaimMappingsOptions>() ?? new ClaimMappingsOptions(); // ============================================================================ // OIDC АУТЕНТИФИКАЦИЯ (ТОЛЬКО COOKIE) // ============================================================================ // Режим работы: // - Пользователь входит через внешний провайдер (PocketID) // - Сессия хранится в cookie .AspNetCore.Cookies // - Браузер автоматически отправляет cookie при каждом запросе // // Подходит для: // - Браузерных приложений // - SPA приложений, которые хотят использовать cookie-сессии // - Приложений с единой точкой входа (SSO) // // ВАЖНО: В этом режиме JWT схема НЕ используется. // Все запросы должны полагаться на cookie для аутентификации. // ============================================================================ services .AddAuthentication(options => { // Используем Cookie схему как схему по умолчанию options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.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 // ============================================================================ .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; } }; }) // ============================================================================ // 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); } } }