Autenticación y autorización en ASP.NET Core SignalR

Autenticación de usuarios que se conectan a un SignalR centro

SignalRpuede usarse con la autenticación ASP.NET Core para asociar un usuario a cada conexión. En un centro de conectividad, se puede acceder a los datos de autenticación desde la HubConnectionContext.User propiedad . La autenticación permite al centro llamar a métodos en todas las conexiones asociadas a un usuario. Para obtener más información, vea Administrar usuarios y grupos en SignalR. Es posible que varias conexiones estén asociadas a un único usuario.

El siguiente código es un ejemplo que usaSignalR y autenticación 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();

Nota:

Si un token expira durante la vigencia de una conexión, la conexión sigue funcionando de forma predeterminada. LongPolling y ServerSentEvent las conexiones producen un error en las solicitudes posteriores si no envían nuevos tokens de acceso. Para que las conexiones se cierren cuando expire el token de autenticación, establezca CloseOnAuthenticationExpiration.

En una aplicación basada en explorador, la cookie autenticación permite que las credenciales de usuario existentes fluyan automáticamente a SignalR las conexiones. Al usar el cliente del explorador, no se necesita ninguna configuración adicional. Si el usuario ha iniciado sesión en una aplicación, la SignalR conexión hereda automáticamente esta autenticación.

Cookies son una manera específica del explorador para enviar tokens de acceso, pero los clientes que no son de explorador pueden enviarlos. Cuando se usa el cliente .NET, la Cookies propiedad se puede configurar en la .WithUrl llamada para proporcionar un cookie. Sin embargo, el uso cookie de la autenticación desde el cliente de .NET requiere que la aplicación proporcione una API para intercambiar datos de autenticación de .cookie

Autenticación por token de portador

El cliente puede proporcionar un token de acceso en lugar de usar uncookie. El servidor valida el token y lo usa para identificar al usuario. Esta validación solo se realiza cuando se establece la conexión. Durante la duración de la conexión, el servidor no vuelve a validar automáticamente para comprobar la revocación de tokens.

En el cliente de JavaScript, el token se puede proporcionar mediante la opción accessTokenFactory .

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

En el cliente .NET, hay una propiedad AccessTokenProvider similar que se puede usar para configurar el token:

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

Nota

Se llama a la función de token de acceso proporcionada antes de cada solicitud HTTP realizada por SignalR. Si el token debe renovarse para mantener la conexión activa, hágalo desde esta función y devuelva el token actualizado. Es posible que el token deba renovarse para que no expire durante la conexión.

En las API web estándar, los tokens de portador se envían en un encabezado HTTP. Sin embargo, SignalR no puede establecer estos encabezados en exploradores al usar algunos transportes. Al usar WebSockets y eventos Server-Sent, el token se transmite como parámetro de cadena de consulta.

Autenticación JWT integrada

En el servidor, la autenticación de token de portador se configura mediante el middleware de 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();

Nota:

La cadena de consulta se usa en exploradores al conectarse con WebSockets y eventos de Server-Sent debido a las limitaciones de la API del explorador. Cuando se usa HTTPS, la conexión TLS protege los valores de cadena de consulta. Sin embargo, muchos servidores registran valores de cadena de consulta. Para obtener más información, vea Consideraciones de seguridad en ASP.NET CoreSignalR. SignalR usa encabezados para transmitir tokens en entornos que los admiten (como los clientes de .NET y Java).

Identity Autenticación JWT de servidor

Al usar Duende IdentityServer, agregue un PostConfigureOptions<TOptions> servicio al proyecto:

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 el servicio después de agregar servicios para la autenticación (AddAuthentication) y el controlador de autenticación para Identity el servidor (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 frente a tokens de portador

Cookies son específicos de los exploradores. Enviarlos desde otros tipos de clientes agrega complejidad en comparación con el envío de tokens de portador. Cookie No se recomienda la autenticación a menos que la aplicación solo necesite autenticar a los usuarios desde el cliente del explorador. La autenticación de token de portador es el enfoque recomendado al usar clientes distintos del cliente del explorador.

Autenticación de Windows

Si autenticación de Windows está configurado en la aplicación, SignalR puede usar esa identidad para proteger los centros de conectividad. Sin embargo, para enviar mensajes a usuarios individuales, agregue un proveedor de identificador de usuario personalizado. El sistema autenticación de Windows no proporciona la notificación "Identificador de nombre". SignalR usa la notificación para determinar el nombre de usuario.

Agregue una nueva clase que implemente IUserIdProvider y recupere una de las notificaciones del usuario para usarlas como identificador. Por ejemplo, para usar la notificación "Name" (que es el nombre de usuario de Windows con el formato [Domain]/[Username]), cree la siguiente clase:

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

En lugar de ClaimTypes.Name, use cualquier valor de User, como el identificador de SID de Windows, etc.

Nota

El valor elegido debe ser único entre todos los usuarios del sistema. De lo contrario, un mensaje destinado a un usuario podría acabar en un usuario diferente.

Registre este componente en 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.

En el cliente .NET, la autenticación de Windows debe estar habilitada estableciendo la UseDefaultCredentials propiedad :

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

autenticación de Windows se admite en Microsoft Edge, pero no en todos los exploradores. Por ejemplo, en Chrome y Safari, se produce un error al intentar usar autenticación de Windows y WebSockets. Cuando se produce un error autenticación de Windows, el cliente intenta revertir a otros transportes que podrían funcionar.

Uso de notificaciones para personalizar el control de identidades

Una aplicación que autentica a los usuarios puede derivar SignalR identificadores de usuario de notificaciones de usuario. Para especificar cómo SignalR crea identificadores de usuario, implemente IUserIdProvider y registre la implementación.

El código de ejemplo muestra cómo usar notificaciones para seleccionar la dirección de correo electrónico del usuario como propiedad de identificación.

Nota

El valor elegido debe ser único entre todos los usuarios del sistema. De lo contrario, un mensaje destinado a un usuario podría acabar en un usuario diferente.

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

El registro de cuenta agrega una notificación con tipo ClaimsTypes.Email a la base de datos de identidad de 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 en Program.cs:

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

Autorización de los usuarios para acceder a centros de conectividad y métodos de concentrador

De forma predeterminada, los usuarios no autenticados pueden llamar a todos los métodos de un servicio. Para requerir autenticación, aplique el AuthorizeAttribute atributo al concentrador:

[Authorize]
public class ChatHub: Hub
{
}

Los argumentos del constructor y las propiedades del[Authorize] atributo pueden usarse para restringir el acceso solo a usuarios que cumplan determinadas directivas de autorización. Por ejemplo, si tiene una directiva de autorización personalizada llamada MyAuthorizationPolicy, asegúrese de que solo los usuarios que coincidan con esa directiva puedan acceder al servicio mediante el código siguiente:

[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.

El [Authorize] atributo se puede aplicar a métodos de concentrador individuales. Si el usuario actual no coincide con la directiva aplicada al método, se devuelve un error a la persona que llama:

[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) ...
    }
}

Uso de controladores de autorización para personalizar la autorización del método central

SignalR proporciona un recurso personalizado a los controladores de autorización cuando un método del centro requiere autorización. El recurso es una instancia de HubInvocationContext. HubInvocationContext Incluye el HubCallerContext, el nombre del método de concentrador que se invoca y los argumentos al método de concentrador.

Considere el ejemplo de un salón de chat que permite el inicio de sesión de varias organizaciones mediante Microsoft Entra ID. Cualquiera con una cuenta de Microsoft puede iniciar sesión en el chat, pero solo los miembros de la organización propietaria deben poder prohibir el acceso a los usuarios o ver sus historiales de chat. Además, es posible que deseemos restringir algunas funcionalidades de usuarios específicos. Tenga en cuenta cómo actúa DomainRestrictedRequirement como un personalizado IAuthorizationRequirement. Ahora que se pasa el HubInvocationContext parámetro de recurso, la lógica interna puede inspeccionar el contexto en el que se llama al centro de conectividad y tomar decisiones sobre cómo permitir que el usuario ejecute métodos de concentrador individuales:

[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));
    }
}

En Program.cs, agregue la nueva directiva y proporcione el requisito personalizado DomainRestrictedRequirement como parámetro para crear la DomainRestricted directiva:

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.

En el ejemplo anterior, la DomainRestrictedRequirement clase es y IAuthorizationRequirement la propia AuthorizationHandler para ese requisito. Es aceptable dividir estos dos componentes en clases independientes para separar los problemas. Una ventaja del enfoque del ejemplo es que no es necesario insertar durante el AuthorizationHandler inicio, ya que el requisito y el controlador son lo mismo.

Recursos adicionales

Vea o descargue el código de ejemplo(cómo descargarlo):

Autenticación de usuarios que se conectan a un SignalR centro

SignalRpuede usarse con la autenticación ASP.NET Core para asociar un usuario a cada conexión. En un centro de conectividad, se puede acceder a los datos de autenticación desde la HubConnectionContext.User propiedad . La autenticación permite al centro llamar a métodos en todas las conexiones asociadas a un usuario. Para obtener más información, vea Administrar usuarios y grupos en SignalR. Es posible que varias conexiones estén asociadas a un único usuario.

El siguiente es un ejemploStartup.Configure de los usoSignalRs y la autenticación de 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?}");
    });
}

Nota:

Si un token expira durante la vigencia de una conexión, la conexión continúa funcionando. LongPolling y ServerSentEvent las conexiones producen un error en las solicitudes posteriores si no envían nuevos tokens de acceso.

En una aplicación basada en explorador, la cookie autenticación permite que las credenciales de usuario existentes fluyan automáticamente a las SignalR conexiones. Al usar el cliente del explorador, no se necesita ninguna configuración adicional. Si el usuario ha iniciado sesión en la aplicación, la SignalR conexión hereda automáticamente esta autenticación.

Cookies son una manera específica del explorador para enviar tokens de acceso, pero los clientes que no son de explorador pueden enviarlos. Cuando se usa el cliente .NET, la Cookies propiedad se puede configurar en la .WithUrl llamada para proporcionar un cookie. Sin embargo, el uso cookie de la autenticación desde el cliente de .NET requiere que la aplicación proporcione una API para intercambiar datos de autenticación de .cookie

Autenticación por token de portador

El cliente puede proporcionar un token de acceso en lugar de usar uncookie. El servidor valida el token y lo usa para identificar al usuario. Esta validación solo se realiza cuando se establece la conexión. Durante la duración de la conexión, el servidor no vuelve a validar automáticamente para comprobar la revocación de tokens.

En el cliente de JavaScript, el token se puede proporcionar mediante la opción accessTokenFactory .

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

En el cliente .NET, hay una propiedad AccessTokenProvider similar que se puede usar para configurar el token:

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

Nota

Se llama a la función de token de acceso que proporcione antes de cada solicitud HTTP realizada por SignalR. Si necesita renovar el token para mantener la conexión activa (porque puede expirar durante la conexión), hágalo desde dentro de esta función y devuelva el token actualizado.

En las API web estándar, los tokens de portador se envían en un encabezado HTTP. Sin embargo, SignalR no puede establecer estos encabezados en exploradores al usar algunos transportes. Al usar WebSockets y eventos Server-Sent, el token se transmite como parámetro de cadena de consulta.

Autenticación JWT integrada

En el servidor, la autenticación de token de portador se configura mediante el middleware de 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. 
}

Si quiere que los comentarios de código se traduzcan en más idiomas además del inglés, háganoslo saber en este problema de debate de GitHub.

Nota

La cadena de consulta se usa en exploradores al conectarse con WebSockets y eventos de Server-Sent debido a las limitaciones de la API del explorador. Cuando se usa HTTPS, la conexión TLS protege los valores de cadena de consulta. Sin embargo, muchos servidores registran valores de cadena de consulta. Para obtener más información, vea Consideraciones de seguridad en ASP.NET CoreSignalR. SignalR usa encabezados para transmitir tokens en entornos que los admiten (como los clientes de .NET y Java).

Identity Autenticación JWT de servidor

Al usar Identity Server, agregue un PostConfigureOptions<TOptions> servicio al proyecto:

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 el servicio en Startup.ConfigureServices después de agregar servicios para la autenticación (AddAuthentication) y el controlador de autenticación para Identity el servidor (AddIdentityServerJwt):

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

Cookies frente a tokens de portador

Cookies son específicos de los exploradores. Enviarlos desde otros tipos de clientes agrega complejidad en comparación con el envío de tokens de portador. Por lo tanto, cookie no se recomienda la autenticación a menos que la aplicación solo necesite autenticar a los usuarios desde el cliente del explorador. La autenticación de token de portador es el enfoque recomendado al usar clientes distintos del cliente del explorador.

Autenticación de Windows

Si la autenticación de Windows está configurado en la aplicación, SignalR puede usar esa identidad para proteger los centros de conectividad. Sin embargo, para enviar mensajes a usuarios individuales, debe agregar un proveedor de identificador de usuario personalizado. El sistema autenticación de Windows no proporciona la notificación "Identificador de nombre". SignalR usa la notificación para determinar el nombre de usuario.

Agregue una nueva clase que implemente IUserIdProvider y recupere una de las notificaciones del usuario para usarlas como identificador. Por ejemplo, para usar la notificación "Name" (que es el nombre de usuario de Windows con el formato [Domain]\[Username]), cree la siguiente clase:

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

En lugar de ClaimTypes.Name, puede usar cualquier valor de User (como el identificador de SID de Windows, etc.).

Nota

El valor que elija debe ser único entre todos los usuarios del sistema. De lo contrario, un mensaje destinado a un usuario podría acabar en un usuario diferente.

Registre este componente en el Startup.ConfigureServices método .

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

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

En el cliente .NET, la autenticación de Windows debe estar habilitada estableciendo la UseDefaultCredentials propiedad :

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

autenticación de Windows se admite en Internet Explorer y Microsoft Edge, pero no en todos los exploradores. Por ejemplo, en Chrome y Safari, se produce un error al intentar usar autenticación de Windows y WebSockets. Cuando se produce un error autenticación de Windows, el cliente intenta revertir a otros transportes que podrían funcionar.

Uso de notificaciones para personalizar el control de identidades

Una aplicación que autentica a los usuarios puede derivar SignalR identificadores de usuario de notificaciones de usuario. Para especificar cómo SignalR crea identificadores de usuario, implemente IUserIdProvider y registre la implementación.

El código de ejemplo muestra cómo usaría las notificaciones para seleccionar la dirección de correo electrónico del usuario como propiedad de identificación.

Nota

El valor que elija debe ser único entre todos los usuarios del sistema. De lo contrario, un mensaje destinado a un usuario podría acabar en un usuario diferente.

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

El registro de cuenta agrega una notificación con tipo ClaimsTypes.Email a la base de datos de identidad de 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 este componente en Startup.ConfigureServices.

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Autorización de los usuarios para acceder a centros de conectividad y métodos de concentrador

De forma predeterminada, los usuarios no autenticados pueden llamar a todos los métodos de un servicio. Para requerir autenticación, aplique el AuthorizeAttribute atributo al concentrador:

[Authorize]
public class ChatHub: Hub
{
}

Puede usar los argumentos de constructor y las propiedades del atributo [Authorize] para restringir el acceso solo a los usuarios que coincidan con directivas de autorización específicas. Por ejemplo, si tiene una directiva de autorización personalizada llamadaMyAuthorizationPolicy puede asegurarse de que solo los usuarios que coincidan con esa directiva puedan acceder al concentrador usando el siguiente código:

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

A los métodos de concentrador individuales también se les[Authorize] puede aplicar el atributo. Si el usuario actual no coincide con la directiva aplicada al método, se devuelve un error a la persona que llama:

[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) ...
    }
}

Uso de controladores de autorización para personalizar la autorización del método central

SignalR proporciona un recurso personalizado a los controladores de autorización cuando un método del centro requiere autorización. El recurso es una instancia de HubInvocationContext. HubInvocationContext Incluye el HubCallerContext, el nombre del método de concentrador que se invoca y los argumentos al método de concentrador.

Considere el ejemplo de un salón de chat que permite el inicio de sesión de varias organizaciones mediante Microsoft Entra ID. Cualquiera con una cuenta de Microsoft puede iniciar sesión en el chat, pero solo los miembros de la organización propietaria deben poder prohibir el acceso a los usuarios o ver sus historiales de chat. Además, es posible que deseemos restringir cierta funcionalidad de determinados usuarios. Con las características actualizadas de ASP.NET Core 3.0, esto es totalmente posible. Tenga en cuenta DomainRestrictedRequirementcómo actúa como un personalizado IAuthorizationRequirement. Ahora que se pasa el HubInvocationContext parámetro de recurso, la lógica interna puede inspeccionar el contexto en el que se llama al centro de conectividad y tomar decisiones sobre cómo permitir que el usuario ejecute métodos de concentrador individuales.

[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));
    }
}

En Startup.ConfigureServices, agregue la nueva directiva y proporcione el requisito personalizado DomainRestrictedRequirement como parámetro para crear la DomainRestricted directiva.

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

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

En el ejemplo anterior, la DomainRestrictedRequirement clase es y IAuthorizationRequirement la propia AuthorizationHandler para ese requisito. Es aceptable dividir estos dos componentes en clases independientes para separar los problemas. Una ventaja del enfoque del ejemplo es que no es necesario insertar durante el AuthorizationHandler inicio, ya que el requisito y el controlador son lo mismo.

Recursos adicionales