Autenticação e autorização no ASP.NET Core SignalR

Autenticar usuários que se conectam a um hub do SignalR

O SignalR pode ser usado com a autenticação do ASP.NET Core para associar um usuário a cada conexão. Em um hub, os dados de autenticação podem ser acessados da propriedade HubConnectionContext.User. A autenticação permite que o hub chame métodos em todas as conexões associadas a um usuário. Para obter mais informações, consulte Gerenciar usuários e grupos no SignalR. Várias conexões podem ser associadas a um único usuário.

O código a seguir é um exemplo que usa o SignalR e a autenticação do ASP.NET Core:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chat");

app.Run();

Observação

Se um token expirar durante o tempo de vida de uma conexão, por padrão, a conexão continuará funcionando. As conexões LongPolling e ServerSentEvent falharão em solicitações subsequentes se não enviarem novos tokens de acesso. Para que as conexões fechem quando o token de autenticação expirar, defina CloseOnAuthenticationExpiration.

Em um aplicativo baseado em navegador, a autenticação de cookie permite que as credenciais de usuário existentes fluam automaticamente para conexões do SignalR. Ao usar o cliente do navegador, nenhuma configuração extra é necessária. Se o usuário estiver conectado a um aplicativo, a conexão do SignalR herdará automaticamente essa autenticação.

Cookies são uma maneira específica do navegador de enviar tokens de acesso, mas clientes que não são do navegador podem enviá-los. Ao usar o Cliente .NET, a propriedade Cookies pode ser configurada na chamada .WithUrl para fornecer um cookie. No entanto, o uso da autenticação de cookie do cliente .NET exige que o aplicativo forneça uma API para trocar dados de autenticação por um cookie.

Autenticação de token de portador

O cliente pode fornecer um token de acesso em vez de usar um cookie. O servidor valida o token e o utiliza para identificar o usuário. Essa validação é feita somente quando a conexão é estabelecida. Durante a vida útil da conexão, o servidor não revalida automaticamente para verificar a revogação de token.

No cliente JavaScript, o token pode ser fornecido usando a opção accessTokenFactory.

// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();

No cliente .NET, há uma propriedade AccessTokenProvider semelhante que pode ser usada para configurar o token:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    { 
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();

Observação

A função do token de acesso fornecida é chamada antes de cada solicitação HTTP feita pelo SignalR. Se o token precisar ser renovado para manter a conexão ativa, faça isso de dentro dessa função e retorne o token atualizado. O token pode precisar ser renovado para que não expire durante a conexão.

Em APIs Web padrão, os tokens de portador são enviados em um cabeçalho HTTP. No entanto, SignalR não pode definir esses cabeçalhos em navegadores ao usar alguns transportes. Ao usar WebSockets e eventos enviados pelo servidor, o token é transmitido como um parâmetro de cadeia de caracteres de consulta.

Autenticação JWT interna

No servidor, a autenticação do token de portador é configurada usando o middleware do Portador JWT:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddAuthentication(options =>
{
    // Identity made Cookie authentication the default.
    // However, we want JWT Bearer Auth to be the default.
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
  {
      // Configure the Authority to the expected value for
      // the authentication provider. This ensures the token
      // is appropriately validated.
      options.Authority = "Authority URL"; // TODO: Update URL

      // We have to hook the OnMessageReceived event in order to
      // allow the JWT authentication handler to read the access
      // token from the query string when a WebSocket or 
      // Server-Sent Events request comes in.

      // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
      // due to a limitation in Browser APIs. We restrict it to only calls to the
      // SignalR hub in this code.
      // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
      // for more information about security considerations when using
      // the query string to transmit the access token.
      options.Events = new JwtBearerEvents
      {
          OnMessageReceived = context =>
          {
              var accessToken = context.Request.Query["access_token"];

              // If the request is for our hub...
              var path = context.HttpContext.Request.Path;
              if (!string.IsNullOrEmpty(accessToken) &&
                  (path.StartsWithSegments("/hubs/chat")))
              {
                  // Read the token out of the query string
                  context.Token = accessToken;
              }
              return Task.CompletedTask;
          }
      };
  });

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

// Change to use Name as the user identifier for SignalR
// WARNING: This requires that the source of your JWT token 
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages 
// intended for a different user!
builder.Services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

// Change to use email as the user identifier for SignalR
// builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

// WARNING: use *either* the NameUserIdProvider *or* the 
// EmailBasedUserIdProvider, but do not use both. 

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();

Observação

A cadeia de caracteres de consulta é usada em navegadores ao se conectar com WebSockets e eventos enviados pelo servidor devido a limitações da API do navegador. Ao usar HTTPS, os valores da cadeia de caracteres de consulta são protegidos pela conexão TLS. No entanto, muitos servidores registram valores de cadeia de caracteres de consulta. Para obter mais informações, consulte Considerações de segurança no SignalR no ASP.NET Core. O SignalR usa cabeçalhos para transmitir tokens em ambientes que dão suporte a eles (como os clientes .NET e Java).

Autenticação JWT do IdentityServer

Ao usar o IdentityServer da Duende, adicione um serviço PostConfigureOptions<TOptions> ao projeto:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

Registre o serviço depois de adicionar serviços para autenticação (AddAuthentication) e o manipulador de autenticação para IdentityServer (AddIdentityServerJwt):

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
        ConfigureJwtBearerOptions>());

builder.Services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.

Cookies versus tokens de portador

Cookies são específicos de navegadores. Enviá-los de outros tipos de clientes adiciona complexidade em comparação com o envio de tokens de portador. A autenticação de Cookie não é recomendada, a menos que o aplicativo só precise autenticar usuários do cliente do navegador. A autenticação de token de portador é a abordagem recomendada ao usar clientes diferentes do cliente do navegador.

Autenticação do Windows

Se a autenticação do Windows estiver configurada no aplicativo, o SignalR poderá usar essa identidade para proteger os hubs. No entanto, para enviar mensagens a usuários individuais, adicione um provedor de ID de usuário personalizado. O sistema de autenticação do Windows não fornece a declaração "Identificador de Nome". O SignalR usa a declaração para determinar o nome de usuário.

Adicione uma nova classe que implementa IUserIdProvider e recupera uma das declarações do usuário a ser usada como o identificador. Por exemplo, para usar a declaração "Nome" (que é o nome de usuário do Windows no formulário [Domain]/[Username]), crie a seguinte classe:

public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}

Em vez de ClaimTypes.Name, use qualquer valor do User, como o identificador de segurança do Windows etc.

Observação

O valor escolhido deve ser exclusivo entre todos os usuários no sistema. Caso contrário, uma mensagem destinada a um usuário pode acabar indo para um usuário diferente.

Registre este componente em Program.cs:

using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.SignalR;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();

services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
services.AddRazorPages();

services.AddSignalR();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

var app = builder.Build();

// Code removed for brevity.

No Cliente .NET, a Autenticação do Windows deve ser habilitada definindo a propriedade UseDefaultCredentials:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();

A autenticação do Windows tem suporte no Microsoft Edge, mas não em todos os navegadores. Por exemplo, no Chrome e no Safari, uma falha ocorre na tentativa de usar autenticação do Windows e WebSockets. Quando autenticação do Windows falha, o cliente tenta fazer fallback para outros transportes que podem funcionar.

Usar declarações para personalizar o tratamento de identidade

Um aplicativo que autentica os usuários pode derivar IDs de usuário do SignalR de declarações de usuário. Para especificar como o SignalR cria IDs de usuário, implemente IUserIdProvider e registre a implementação.

O código de exemplo demonstra como usar declarações para selecionar o endereço de email do usuário como a propriedade de identificação.

Observação

O valor escolhido deve ser exclusivo entre todos os usuários no sistema. Caso contrário, uma mensagem destinada a um usuário pode acabar indo para um usuário diferente.

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value!;
    }
}

O registro da conta adiciona uma declaração com o tipo ClaimsTypes.Email ao banco de dados de identidade do ASP.NET.

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
                                                                          .ToList();
    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
        var result = await _userManager.CreateAsync(user, Input.Password);

        // Add the email claim and value for this user.
        await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

        // Remaining code removed for brevity.

Registre este componente em Program.cs:

builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Autorizar usuários a acessar hubs e métodos de hub

Por padrão, todos os métodos em um hub podem ser chamados por um usuário não autenticado. Para exigir autenticação, aplique o atributo AuthorizeAttribute ao hub:

[Authorize]
public class ChatHub: Hub
{
}

Os argumentos do construtor e as propriedades do atributo [Authorize] podem ser usados para restringir o acesso somente a usuários que correspondam a políticas de autorização específicas. Por exemplo, com a política de autorização personalizada chamada MyAuthorizationPolicy, somente os usuários correspondentes a essa política podem acessar o hub usando o seguinte código:

[Authorize("MyAuthorizationPolicy")]
public class ChatPolicyHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Clients.All.SendAsync("ReceiveSystemMessage", 
                                    $"{Context.UserIdentifier} joined.");
        await base.OnConnectedAsync();
    }
    // Code removed for brevity.

O atributo [Authorize] pode ser aplicado a métodos de hub individuais. Se o usuário atual não corresponder à política aplicada ao método, um erro será retornado ao chamador:

[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... ban a user from the chat room (something only Administrators can do) ...
    }
}

Usar manipuladores de autorização para personalizar a autorização do método de hub

O SignalR fornece um recurso personalizado para manipuladores de autorização quando um método de hub exige autorização. O recurso é uma instância do HubInvocationContext. O HubInvocationContext inclui o HubCallerContext, o nome do método de hub que está sendo invocado e os argumentos para o método de hub.

Considere o exemplo de uma sala de chat que permite a entrada de várias organizações por meio do Microsoft Entra ID. Qualquer pessoa com uma conta Microsoft pode entrar no chat, mas apenas os membros da organização proprietária devem ser capazes de proibir usuários ou exibir históricos de chat dos usuários. Além disso, talvez seja importante restringir algumas funcionalidades de usuários específicos. Observe como o DomainRestrictedRequirement serve como um IAuthorizationRequirement personalizado. Agora que o parâmetro de recurso HubInvocationContext está sendo passado, a lógica interna pode inspecionar o contexto no qual o Hub está sendo chamado e tomar decisões sobre como permitir que o usuário execute métodos de hub individuais:

[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

namespace SignalRAuthenticationSample;

public class DomainRestrictedRequirement :
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>,
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement,
        HubInvocationContext resource)
    {
        if (context.User.Identity != null &&
          !string.IsNullOrEmpty(context.User.Identity.Name) && 
          IsUserAllowedToDoThis(resource.HubMethodName,
                               context.User.Identity.Name) &&
          context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
                context.Succeed(requirement);
            
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") &&
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}

Em Program.cs, adicione a nova política, fornecendo o requisito personalizado DomainRestrictedRequirement como um parâmetro para criar a política DomainRestricted:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var services = builder.Services;

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
services.AddDatabaseDeveloperPageExceptionFilter();

services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthorization(options =>
   {
       options.AddPolicy("DomainRestricted", policy =>
       {
           policy.Requirements.Add(new DomainRestrictedRequirement());
       });
   });

services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.

No exemplo anterior, a classe DomainRestrictedRequirement é um IAuthorizationRequirement e seu próprio AuthorizationHandler para esse requisito. É aceitável dividir esses dois componentes em classes separadas para separar preocupações. Um benefício da abordagem do exemplo é que não há necessidade de injetar o AuthorizationHandler durante a inicialização, pois o requisito e o manipulador são a mesma coisa.

Recursos adicionais

Exibir ou fazer download do código de exemplo(como fazer download)

Autenticar usuários que se conectam a um hub do SignalR

O SignalR pode ser usado com a autenticação do ASP.NET Core para associar um usuário a cada conexão. Em um hub, os dados de autenticação podem ser acessados da propriedade HubConnectionContext.User. A autenticação permite que o hub chame métodos em todas as conexões associadas a um usuário. Para obter mais informações, consulte Gerenciar usuários e grupos no SignalR. Várias conexões podem ser associadas a um único usuário.

Veja a seguir um exemplo de Startup.Configure, que usa o SignalR e a autenticação do ASP.NET Core:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chat");
        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}

Observação

Se um token expirar durante o tempo de vida de uma conexão, ela continuará funcionando. As conexões LongPolling e ServerSentEvent falharão em solicitações subsequentes se não enviarem novos tokens de acesso.

Em um aplicativo baseado em navegador, a autenticação de cookie permite que as credenciais de usuário existentes fluam automaticamente para conexões do SignalR. Ao usar o cliente do navegador, nenhuma configuração adicional é necessária. Se o usuário estiver conectado a um aplicativo, a conexão do SignalR herdará automaticamente essa autenticação.

Cookies são uma maneira específica do navegador de enviar tokens de acesso, mas clientes que não são do navegador podem enviá-los. Ao usar o Cliente .NET, a propriedade Cookies pode ser configurada na chamada .WithUrl para fornecer um cookie. No entanto, o uso da autenticação de cookie do cliente .NET exige que o aplicativo forneça uma API para trocar dados de autenticação por um cookie.

Autenticação de token de portador

O cliente pode fornecer um token de acesso em vez de usar um cookie. O servidor valida o token e o utiliza para identificar o usuário. Essa validação é feita somente quando a conexão é estabelecida. Durante a vida útil da conexão, o servidor não revalida automaticamente para verificar a revogação de token.

No cliente JavaScript, o token pode ser fornecido usando a opção accessTokenFactory.

// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();

No cliente .NET, há uma propriedade AccessTokenProvider semelhante que pode ser usada para configurar o token:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    { 
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();

Observação

A função do token de acesso fornecida é chamada antes de cada solicitação HTTP feita pelo SignalR. Se você precisar renovar o token para manter a conexão ativa (porque ela pode expirar durante a conexão), faça isso de dentro dessa função e retorne o token atualizado.

Em APIs Web padrão, os tokens de portador são enviados em um cabeçalho HTTP. No entanto, SignalR não pode definir esses cabeçalhos em navegadores ao usar alguns transportes. Ao usar WebSockets e eventos enviados pelo servidor, o token é transmitido como um parâmetro de cadeia de caracteres de consulta.

Autenticação JWT interna

No servidor, a autenticação do token de portador é configurada usando o middleware do Portador JWT:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication(options =>
        {
            // Identity made Cookie authentication the default.
            // However, we want JWT Bearer Auth to be the default.
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            // Configure the Authority to the expected value for your authentication provider
            // This ensures the token is appropriately validated
            options.Authority = /* TODO: Insert Authority URL here */;

            // We have to hook the OnMessageReceived event in order to
            // allow the JWT authentication handler to read the access
            // token from the query string when a WebSocket or 
            // Server-Sent Events request comes in.

            // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
            // due to a limitation in Browser APIs. We restrict it to only calls to the
            // SignalR hub in this code.
            // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
            // for more information about security considerations when using
            // the query string to transmit the access token.
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query["access_token"];

                    // If the request is for our hub...
                    var path = context.HttpContext.Request.Path;
                    if (!string.IsNullOrEmpty(accessToken) &&
                        (path.StartsWithSegments("/hubs/chat")))
                    {
                        // Read the token out of the query string
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                }
            };
        });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddSignalR();

    // Change to use Name as the user identifier for SignalR
    // WARNING: This requires that the source of your JWT token 
    // ensures that the Name claim is unique!
    // If the Name claim isn't unique, users could receive messages 
    // intended for a different user!
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

    // Change to use email as the user identifier for SignalR
    // services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

    // WARNING: use *either* the NameUserIdProvider *or* the 
    // EmailBasedUserIdProvider, but do not use both. 
}

Se você quiser ver os comentários de código traduzidos para idiomas diferentes do inglês, informe-nos neste problema de discussão do GitHub.

Observação

A cadeia de caracteres de consulta é usada em navegadores ao se conectar com WebSockets e eventos enviados pelo servidor devido a limitações da API do navegador. Ao usar HTTPS, os valores da cadeia de caracteres de consulta são protegidos pela conexão TLS. No entanto, muitos servidores registram valores de cadeia de caracteres de consulta. Para obter mais informações, consulte Considerações de segurança no SignalR no ASP.NET Core. O SignalR usa cabeçalhos para transmitir tokens em ambientes que dão suporte a eles (como os clientes .NET e Java).

Autenticação JWT do IdentityServer

Ao usar o IdentityServer, adicione um serviço PostConfigureOptions<TOptions> ao projeto:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) && 
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

Registre o serviço em Startup.ConfigureServices depois de adicionar serviços para autenticação (AddAuthentication) e o manipulador de autenticação para IdentityServer (AddIdentityServerJwt):

services.AddAuthentication()
    .AddIdentityServerJwt();
services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, 
        ConfigureJwtBearerOptions>());

Cookies versus tokens de portador

Cookies são específicos de navegadores. Enviá-los de outros tipos de clientes adiciona complexidade em comparação com o envio de tokens de portador. Consequentemente, a autenticação de cookie não é recomendada, a menos que o aplicativo só precise autenticar usuários do cliente do navegador. A autenticação de token de portador é a abordagem recomendada ao usar clientes diferentes do cliente do navegador.

Autenticação do Windows

Se a autenticação do Windows estiver configurada no aplicativo, o SignalR poderá usar essa identidade para proteger os hubs. No entanto, para enviar mensagens a usuários individuais, você precisa adicionar um provedor de ID de Usuário personalizado. O sistema de autenticação do Windows não fornece a declaração "Identificador de Nome". O SignalR usa a declaração para determinar o nome de usuário.

Adicione uma nova classe que implementa IUserIdProvider e recupera uma das declarações do usuário a ser usada como o identificador. Por exemplo, para usar a declaração "Nome" (que é o nome de usuário do Windows no formulário [Domain]\[Username]), crie a seguinte classe:

public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}

Em vez de ClaimTypes.Name, use qualquer valor do User (como o identificador de segurança do Windows etc.)

Observação

O valor escolhido deve ser exclusivo entre todos os usuários em seu sistema. Caso contrário, uma mensagem destinada a um usuário pode acabar indo para um usuário diferente.

Registre esse componente em seu método Startup.ConfigureServices.

public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

    services.AddSignalR();
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}

No Cliente .NET, a Autenticação do Windows deve ser habilitada definindo a propriedade UseDefaultCredentials:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();

A autenticação do Windows tem suporte no Internet Explorer e no Microsoft Edge, mas não em todos os navegadores. Por exemplo, no Chrome e no Safari, uma falha ocorre na tentativa de usar autenticação do Windows e WebSockets. Quando autenticação do Windows falha, o cliente tenta fazer fallback para outros transportes que podem funcionar.

Usar declarações para personalizar o tratamento de identidade

Um aplicativo que autentica os usuários pode derivar IDs de usuário do SignalR de declarações de usuário. Para especificar como o SignalR cria IDs de usuário, implemente IUserIdProvider e registre a implementação.

O código de exemplo demonstra como você usaria declarações para selecionar o endereço de email do usuário como a propriedade de identificação.

Observação

O valor escolhido deve ser exclusivo entre todos os usuários em seu sistema. Caso contrário, uma mensagem destinada a um usuário pode acabar indo para um usuário diferente.

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
    }
}

O registro da conta adiciona uma declaração com o tipo ClaimsTypes.Email ao banco de dados de identidade do ASP.NET.

// create a new user
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);

// add the email claim and value for this user
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

Registre esse componente em Startup.ConfigureServices.

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Autorizar usuários a acessar hubs e métodos de hub

Por padrão, todos os métodos em um hub podem ser chamados por um usuário não autenticado. Para exigir autenticação, aplique o atributo AuthorizeAttribute ao hub:

[Authorize]
public class ChatHub: Hub
{
}

Você pode usar os argumentos do construtor e as propriedades do atributo [Authorize] para restringir o acesso somente a usuários que correspondem a políticas de autorização específicas. Por exemplo, se você tiver uma política de autorização personalizada chamada MyAuthorizationPolicy, verifique se somente os usuários correspondentes a essa política poderão acessar o serviço usando o seguinte código:

[Authorize("MyAuthorizationPolicy")]
public class ChatHub : Hub
{
}

Métodos de hub individuais também podem ter o atributo [Authorize] aplicado. Se o usuário atual não corresponder à política aplicada ao método, um erro será retornado ao chamador:

[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... ban a user from the chat room (something only Administrators can do) ...
    }
}

Usar manipuladores de autorização para personalizar a autorização do método de hub

O SignalR fornece um recurso personalizado para manipuladores de autorização quando um método de hub exige autorização. O recurso é uma instância do HubInvocationContext. O HubInvocationContext inclui o HubCallerContext, o nome do método de hub que está sendo invocado e os argumentos para o método de hub.

Considere o exemplo de uma sala de chat que permite a entrada de várias organizações por meio do Microsoft Entra ID. Qualquer pessoa com uma conta Microsoft pode entrar no chat, mas apenas os membros da organização proprietária devem ser capazes de proibir usuários ou exibir históricos de chat dos usuários. Além disso, talvez seja importante restringir determinadas funcionalidades para determinados usuários. Usando os recursos atualizados no ASP.NET Core 3.0, isso é totalmente possível. Observe como o DomainRestrictedRequirement serve como um IAuthorizationRequirement personalizado. Agora que o parâmetro de recurso HubInvocationContext está sendo passado, a lógica interna pode inspecionar o contexto no qual o Hub está sendo chamado e tomar decisões sobre como permitir que o usuário execute métodos de hub individuais.

[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}

public class DomainRestrictedRequirement : 
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>, 
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement, 
        HubInvocationContext resource)
    {
        if (IsUserAllowedToDoThis(resource.HubMethodName, context.User.Identity.Name) && 
            context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") && 
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}

Em Startup.ConfigureServices, adicione a nova política, fornecendo o requisito personalizado DomainRestrictedRequirement como um parâmetro para criar a política DomainRestricted.

public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

    services
        .AddAuthorization(options =>
        {
            options.AddPolicy("DomainRestricted", policy =>
            {
                policy.Requirements.Add(new DomainRestrictedRequirement());
            });
        });
}

No exemplo anterior, a classe DomainRestrictedRequirement é um IAuthorizationRequirement e seu próprio AuthorizationHandler para esse requisito. É aceitável dividir esses dois componentes em classes separadas para separar preocupações. Um benefício da abordagem do exemplo é que não há necessidade de injetar o AuthorizationHandler durante a inicialização, pois o requisito e o manipulador são a mesma coisa.

Recursos adicionais