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.