Start Debugging

SignalR в .NET 11 RC 1 меняет истекающий токен, не разрывая соединение

API обновления аутентификации SignalR финализированы в .NET 11 RC 1: переименованные обратные вызовы у HubConnection, клиент на TypeScript и цепи Blazor Server, которые автоматически подхватывают обновлённый ClaimsPrincipal.

Раздел ASP.NET Core в .NET 11 Release Candidate 1 открывается работой над обновлением аутентификации, которая ведётся начиная с Preview 6: соединение SignalR теперь может заменить истекающий access token на месте, а RC 1 фиксирует форму API сервера и клиента .NET (dotnet/aspnetcore#68702). Это тот же релиз, в котором сигналы POSIX появились у System.Diagnostics.Process, и он выходит с лицензией go-live.

Почему оба прежних варианта были плохими

Раньше хаб с токенами bearer оставлял два пути, и ни один не был хорошим. Если оставить CloseOnAuthenticationExpiration в значении по умолчанию, соединение продолжало работать под уже истёкшим ClaimsPrincipal, то есть решения авторизации принимались по устаревшим claims. Если выставить true, SignalR закрывал соединение ровно в момент истечения токена, а это полное переподключение: OnConnectedAsync выполняется заново, членство в группах приходится восстанавливать, и всё, что клиент получал потоком, обрывается.

Для дашборда с 15-минутным токеном это заметный рывок каждые 15 минут без всякой причины, кроме гигиены токенов.

Включение на стороне сервера

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

using System.Security.Claims;

app.MapHub<ClockHub>("/clock", options =>
{
    options.EnableAuthenticationRefresh = true;
    options.CloseOnAuthenticationExpiration = true;
    options.OnAuthenticationRefresh = context =>
    {
        var previousSubject = context.PreviousUser.FindFirstValue("sub")
            ?? context.PreviousUser.FindFirstValue(ClaimTypes.NameIdentifier);
        var newSubject = context.NewUser.FindFirstValue("sub")
            ?? context.NewUser.FindFirstValue(ClaimTypes.NameIdentifier);

        return Task.FromResult(
            previousSubject is not null &&
            string.Equals(previousSubject, newSubject, StringComparison.Ordinal));
    };
});

Возврат false здесь и есть самое важное. Без этой проверки клиент мог бы передать вам действительный токен совершенно другого пользователя и сохранить соединение вместе с его членством в группах от первой личности.

Сторона клиента и что переименовали

Клиент .NET планирует обновление заранее, до истечения срока, и отдаёт результат как события у HubConnection:

await using var connection = new HubConnectionBuilder()
    .WithUrl(serverUrl, options =>
        options.AccessTokenProvider = GetAccessTokenAsync)
    .WithAuthenticationRefresh(options =>
    {
        options.EnableAutoRefresh = true;
        options.RefreshBeforeExpiration = TimeSpan.FromMinutes(2);
    })
    .Build();

connection.AuthenticationRefreshed += context =>
{
    Console.WriteLine($"New token lifetime: {context.NewTokenLifetime}");
    return Task.CompletedTask;
};

await connection.StartAsync();

// Force a refresh right after acquiring a token with updated claims.
await connection.RefreshAuthenticationAsync();

Если вы уже были на Preview 7, три вещи переехали. OnAuthenticationRefreshed и OnAuthenticationRefreshFailed больше не обратные вызовы у AuthenticationRefreshOptions, это события HubConnection.AuthenticationRefreshed и HubConnection.AuthenticationRefreshFailed из примера выше. AuthenticationRefreshContext переехал из Microsoft.AspNetCore.Http.Connections в Microsoft.AspNetCore.Connections.Features. А своим транспортам нужен IConnectionAuthenticationRefreshFeature вместо IConnectionUserRefreshFeature.

Клиент на TypeScript получает ту же форму через withAuthenticationRefresh({ enableAutoRefresh: true, refreshBeforeExpirationInMilliseconds: 120_000 }) (dotnet/aspnetcore#67964).

Blazor Server получает это бесплатно

Компонентам Interactive Server не нужна никакая настройка (dotnet/aspnetcore#68221). Хаб компонентов включает обновление сам, и когда токен заменяется, Blazor обновляет состояние аутентификации и вызывает AuthenticationStateChanged, так что AuthorizeView и всё остальное, что использует AuthenticationStateProvider, перерисовывается уже по новым claims. Это наконец превращает “роль пользователя изменилась посреди сессии” в то, что можно отразить, не разрушая цепь.

Полные подробности есть в заметках о выпуске ASP.NET Core для RC 1.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад