Как в C# в обработчике события AddCookie[options.Events.OnValidatePrincipal] можно проверить активность сессии пользователя на стороннем сервере аутентификации?

Привет, дорогой друг! <br/> <br/> Не судите строго. Позвольте поделиться полной историей: <br/> <br/> У меня есть backend на C# (.NET), который использует сторонний сервис для аутентификации и авторизации. Также имеется frontend, обслуживающий все сервисы. <br/> <br/> В этом случае backend на C# (.NET) выступает в роли клиента OAuth2 для сервера аутентификации. <br/> <br/> <pre><code class="cs">self.AddAuthentication(options =&gt;
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddCookie(options =&gt;
            {
                options.Events.OnValidatePrincipal = context =&gt;
                {
                    var descriptor = context.HttpContext.GetEndpoint()?.Metadata.OfType&lt;ControllerActionDescriptor&gt;().FirstOrDefault();

                    if (descriptor != null &amp;&amp; DISABLEDCONTROLLER.TryGetValue(descriptor.ControllerTypeInfo, out var exception) &amp;&amp;
                        exception.Contains(descriptor.MethodInfo.Name))
                    {
                        return Task.CompletedTask;
                    }

                    var principalValidator = context.HttpContext.RequestServices.GetRequiredService&lt;PrincipalValidator&gt;();
                    return principalValidator.ValidateAsync(context);
                };

                options.SessionStore = self.BuildServiceProvider().GetRequiredService&lt;ITicketStore&gt;();
                options.Cookie.HttpOnly = true;
                options.Cookie.Name = authorizationService.CookieName;
                options.Cookie.Domain = authorizationService.CookieDomain;

                options.Events.OnRedirectToAccessDenied = context =&gt;
                {
  • 3 сентября 2024 г. 12:02
Ответы на вопрос 2
В обработчике события `OnValidatePrincipal`, предоставляемом в конфигурации аутентификации Cookie в ASP.NET Core, вы можете проверить активность сессии пользователя на стороннем сервере аутентификации. Для этого вам нужно будет осуществить HTTP-запрос к вашему серверу аутентификации для получения информации о пользователе. 

Ниже приведён пример того, как это можно реализовать. Предположим, у вас есть метод для проверки состояния сессии на стороннем сервере, например `IsSessionActive`, который возвращает `true`, если сессия пользователя активна, и `false` в противном случае.

Вот пример кода:

```csharp
self.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.Events.OnValidatePrincipal = async context =>
    {
        var descriptor = context.HttpContext.GetEndpoint()?.Metadata
            .OfType<ControllerActionDescriptor>().FirstOrDefault();

        if (descriptor != null && DISABLEDCONTROLLER.TryGetValue(descriptor.ControllerTypeInfo, out var exception) &&
            exception.Contains(descriptor.MethodInfo.Name))
        {
            return;
        }

        var principalValidator = context.HttpContext.RequestServices.GetRequiredService<PrincipalValidator>();
        var result = await principalValidator.ValidateAsync(context);

        // Допустим, у вас есть доступ к HttpClient для совершения запросов к вашему серверу аутентификации
        var httpClient = context.HttpContext.RequestServices.GetRequiredService<HttpClient>();
        var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        if (userId != null)
        {
            // Здесь вызываете метод для проверки активности сессии
            var isActive = await IsSessionActive(httpClient, userId);

            if (!isActive)
            {
                context.RejectPrincipal(); // Отклоняем текущий Principal, т.е. пользователь не авторизован
                await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            }
        }
    };

    options.SessionStore = self.BuildServiceProvider().GetRequiredService<ITicketStore>();
    options.Cookie.HttpOnly = true;
    options.Cookie.Name = authorizationService.CookieName;
    options.Cookie.Domain = authorizationService.CookieDomain;

    options.Events.OnRedirectToAccessDenied = context =>
    {
        context.Response.StatusCode = StatusCodes.Status403Forbidden;
        return Task.CompletedTask;
    };
});

// Пример метода для проверки активности сессии
private async Task<bool> IsSessionActive(HttpClient httpClient, string userId)
{
    var response = await httpClient.GetAsync($"https://your-auth-server/api/session/status/{userId}");
    if (response.IsSuccessStatusCode)
    {
        var result = await response.Content.ReadAsStringAsync();
        // Считаем, что ответ JSON и возвращает поле "isActive"
        return JsonConvert.DeserializeObject<SessionStatusResponse>(result).IsActive;
    }

    return false;
}

// Вспомогательный класс для десериализации ответа
public class SessionStatusResponse
{
    public bool IsActive { get; set; }
}
```

В этом примере:
- Мы используем `HttpClient` для выполнения GET-запроса на сервер аутентификации с информацией о пользователе (например, его ID).
- Метод `IsSessionActive` возвращает `true`, если сессия активна, или `false`, если нет.
- Если сессия не активна, мы отклоняем текущий `ClaimsPrincipal` и вызываем выход пользователя из системы.

Не забудьте установить `HttpClient` в контейнере DI, если вы его ещё не сделали.
Подскажите, как делают refresh_token взрослые дяди в многопоточке.

Если нужен ответ на это конкретный вопрос, причем если имеется в виду не просто многопоточка, а асинхронная, с await без блокировки потока - то на это есть такой SemaphoreSlim. Делается примерно так (надеюсь, идея будет понятна)
//Попадаем сюда после получения существующего access token и выявления, что он просрочен
    SemaphoreSlim sem = GetSemaphore(clientId); 
   await sem.WaitAsync(); //timeot и CancellationToken добавить по вкусу
   try {
     //Получаем существующий access token повторно - вдруг его уже кто-нибудь до нас обновил 
    //   (используем double check pattern)
    //Если это не так,  выполняем тут всю логику обновления access token
  }
  finally {
      sem.Release(); //SemaphoreSlim - не мьютекс, сам не освободится в случае чего
  }


GetSemaphore реализовать можно по-разному. Можно один на все приложение: static или Singleton - это если нагрузка небольшая.
А можно кэшировать семафоры по одному для каждого пользователя (т.е. свой семафор для каждого clientID), чтобы пользователи не толклись около одного семафора на всех.
Главное, чтобы этот семафор создавался с начальным значением 1 - тогда он будет пускать пользователей по одному.

Ну, а если все делать без асинхронности, в одном потоке, блокируя его при необходимости (т.е. без await), то способов много. Простейший - блок lock вокруг кода обновления маркера доступа (access token), есть такде Monitor, Mutex, тот же Semaphore (хоть со Slim, хоть без)...
Похожие вопросы