Autenticación JWT en SignalR para ASP.NET Core

Para proteger los hubs de SignalR, es crucial implementar autenticación y evitar conexiones no autorizadas. SignalR permite usar mecanismos como JWT para transmitir identidades de usuario. A continuación, se explica cómo configurar la autenticación JWT con SignalR.

Configuración de opciones JWT

Defina una sección de configuración para JWT en el archivo de configuración, y cree una clase correspondiente con propiedades para la clave de firma y la duración del token.

public class ConfiguracionJWT
{
    public string ClaveFirma { get; set; }
    public int DuracionSegundos { get; set; }
}

Instalación del paquete NuGet

Instale el paquete necesario mediante NuGet Package Manager:

Microsoft.AspNetCore.Authentication.JwtBearer

Configuración de autenticación JWT en Program.cs

Agregue la configuración de JWT antes de construir la aplicación, ajustando los parámetros de validación y manejando el token en solicitudes WebSocket.

var servicios = builder.Services;
servicios.Configure<ConfiguracionJWT>(builder.Configuration.GetSection("JWT"));
servicios.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opciones =>
    {
        var configJWT = builder.Configuration.GetSection("JWT").Get<ConfiguracionJWT>();
        byte[] bytesLlave = Encoding.UTF8.GetBytes(configJWT.ClaveFirma);
        var claveSimetrica = new SymmetricSecurityKey(bytesLlave);
        opciones.TokenValidationParameters = new()
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = claveSimetrica
        };
        opciones.Events = new JwtBearerEvents
        {
            OnMessageReceived = contexto =>
            {
                var tokenAcceso = contexto.Request.Query["access_token"];
                var ruta = contexto.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(tokenAcceso) &&
                    ruta.StartsWithSegments("/Hubs/SalaChat"))
                {
                    contexto.Token = tokenAcceso;
                }
                return Task.CompletedTask;
            }
        };
    });

En aplicaciones web tradicionales, el JWT se envía en el encabezado Authorization, pero WebSocket no soporta encabezados personalizados. Por ello, se pasa el token como parámetro en la URL y se extrae manualmente durante la conexión al hub.

Middleware de autenticación

En Program.cs, asegúrese de agregar el middleawre de autenticación antes de la autorización:

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

Endpoint para inicio de sesión

Cree un método en un controlador para autenticar usuarios y generar tokens JWT.

[HttpPost]
public async Task<IActionResult> Autenticar(SolicitudLogin solicitud,
    [FromServices] IOptions<ConfiguracionJWT> opcionesJWT)
{
    string usuario = solicitud.Usuario;
    string clave = solicitud.Clave;
    Usuario? usuarioEncontrado = RepositorioUsuarios.BuscarPorNombre(usuario);
    if (usuarioEncontrado == null || usuarioEncontrado.Clave != clave)
    {
        return BadRequest("Credenciales inválidas");
    }
    var reclamos = new List<Claim>
    {
        new Claim(ClaimTypes.Name, usuario),
        new Claim(ClaimTypes.NameIdentifier, usuarioEncontrado.Id.ToString())
    };
    string token = CrearTokenJWT(reclamos, opcionesJWT.Value);
    return Ok(token);
}
private static string CrearTokenJWT(IEnumerable<Claim> reclamos, ConfiguracionJWT config)
{
    DateTime expira = DateTime.Now.AddSeconds(config.DuracionSegundos);
    byte[] bytesLlave = Encoding.UTF8.GetBytes(config.ClaveFirma);
    var claveSimetrica = new SymmetricSecurityKey(bytesLlave);
    var credenciales = new SigningCredentials(claveSimetrica,
        SecurityAlgorithms.HmacSha256Signature);
    var descriptorToken = new JwtSecurityToken(expires: expira,
        signingCredentials: credenciales, claims: reclamos);
    return new JwtSecurityTokenHandler().WriteToken(descriptorToken);
}

Protección del hub con autorización

Aplique el atributo [Authorize] a la clase del hub para restringir el acceso a usuarios autenticados.

[Authorize]
public class SalaChatHub : Hub
{
    public Task EnviarMensajePublico(string texto)
    {
        string nombre = Context.User!.FindFirst(ClaimTypes.Name)!.Value;
        string mensaje = $"{nombre}: {texto}";
        return Clients.All.SendAsync("RecibirMensaje", mensaje);
    }
}

Si el atributo solo se coloca en métodos específicos, cualquier cliente podría conectarse al hub sin autenticación, lo que representa un riesgo de seguridad. Se recomienda aplicarlo a la clase completa.

Integración en el frontend con Vue.js

Modifique el código del cliente para manejar la autenticación y establecer la conexión SignalR con el token JWT.

<template>
    <div>
        <label>Usuario: <input v-model="estado.datosLogin.usuario" /></label>
        <label>Contraseña: <input type="password" v-model="estado.datosLogin.clave" /></label>
        <button @click="manejarLogin">Iniciar sesión</button>
    </div>
    <div>
        <input v-model="estado.mensajeActual" @keyup.enter="enviarMensaje" />
        <ul>
            <li v-for="(msg, idx) in estado.historialMensajes" :key="idx">{{ msg }}</li>
        </ul>
    </div>
</template>
<script>
import { reactive } from 'vue';
import * as signalR from '@microsoft/signalr';
import axios from 'axios';
export default {
    setup() {
        const estado = reactive({
            token: null,
            mensajeActual: '',
            historialMensajes: [],
            datosLogin: { usuario: '', clave: '' }
        });
        let conexionHub = null;
        const iniciarConexion = async () => {
            if (!estado.token) return;
            const configTransporte = { accessTokenFactory: () => estado.token };
            conexionHub = new signalR.HubConnectionBuilder()
                .withUrl('https://localhost:7002/Hubs/SalaChat', configTransporte)
                .withAutomaticReconnect()
                .build();
            conexionHub.on('RecibirMensaje', (msg) => {
                estado.historialMensajes.push(msg);
            });
            try {
                await conexionHub.start();
                console.log('Conexión establecida');
            } catch (error) {
                console.error('Error de conexión:', error);
            }
        };
        const manejarLogin = async () => {
            const respuesta = await axios.post('/api/autenticar', estado.datosLogin);
            estado.token = respuesta.data;
            await iniciarConexion();
        };
        const enviarMensaje = async () => {
            if (conexionHub && estado.mensajeActual) {
                await conexionHub.invoke('EnviarMensajePublico', estado.mensajeActual);
                estado.mensajeActual = '';
            }
        };
        return { estado, manejarLogin, enviarMensaje };
    }
};
</script>

El frontend solicita el token JWT mediante el endpoint de autenticación y lo incluye en la conexión SignalR a través de la URL, permitiendo una comunicación segura con el hub.

Etiquetas: SignalR JWT ASP.NET Core autenticación WebSocket

Publicado el 6-23 03:13