Estoy intentando conectar un cliente frontend (Vue.js) a un Hub de SignalR 9 en ASP.NET Core 10. El cliente intenta conectar mediante WebSockets, falla con un código 302, y al caer en el transporte de respaldo (LongPolling), el servidor responde con un 400 bad request durante el intercambio de protocolos (Handshake).
Lo extraño es que los puntos de interrupción dentro de la clase del Hub nunca se detienen, lo que indica que la petición es rechazada por algún componente del pipeline antes de llegar al Hub. Las cookies de sesión sí se están enviando correctamente en los headers de la petición.
Entorno
Backend: ASP.NET Core 9.0 (SignalR v9.0.6)
Frontend: Vue.js con @microsoft/signalr
mi programs esta estructurado asi
using GraphQL.Client;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Localization.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Globalization;
using System.Reflection;
using System.Threading.Tasks;
using TuFactoring.CustomProviders;
using TuFactoring.Data;
using TuFactoring.Resources;
using TuFactoring.Services;
using TuFactoring.Utilities;
using TuFactoring.WebSocket;
using TuFactoringGraphql;
using TuFactoringModels;
InitialConfigurations.SetPort();
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
builder.WebHost.UseUrls("http://0.0.0.0:" + InitialConfigurations.GetPort());
#region CORS & Politica de Cookies
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsDevPolicy", policy =>
{
policy.SetIsOriginAllowed(origin => true)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
```});
builder.Services.Configure<CookiePolicyOptions>(options =>
{
```sql
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
```});
#endregion
#region Identity & Autenticación
builder.Services.AddIdentity<User, Role>(config =>
{
```ruby
config.Password.RequiredLength = 8;
config.Password.RequireDigit = true;
config.Password.RequireUppercase = true;
config.Password.RequireLowercase = true;
config.Password.RequireNonAlphanumeric = true;
```})
.AddDefaultTokenProviders()
.AddClaimsPrincipalFactory<CustomClaimsStore>();
builder.Services.AddTransient<IUserStore<User>, CustomUserStore>();
builder.Services.AddTransient<IRoleStore<Role>, CustomRoleStore>();
builder.Services.ConfigureApplicationCookie(ops =>
{
```powershell
InitialConfigurations.SetTimeSesion(480);
InitialConfigurations.SetFinancing();
InitialConfigurations.SetWallet();
ops.Cookie.Path = "/";
ops.Cookie.IsEssential = true;
ops.Cookie.HttpOnly = false;
ops.Cookie.SameSite = SameSiteMode.Lax;
ops.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
ops.ExpireTimeSpan = TimeSpan.FromMinutes(InitialConfigurations.GetTimeSesion());
ops.LoginPath = $"/{CultureInfo.CurrentCulture.Name}/Identity/Account/Login";
ops.AccessDeniedPath = $"/{CultureInfo.CurrentCulture.Name}/Identity/Account/Logout";
ops.SlidingExpiration = true;
// Evitar redirecciones 302 en llamadas de SignalR
ops.Events.OnRedirectToLogin = context =>
{
var path = context.Request.Path.Value ?? string.Empty;
var pathBase = context.Request.PathBase.Value ?? string.Empty;
if (path.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) ||
pathBase.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) ||
path.Contains("/wsUser", StringComparison.OrdinalIgnoreCase) ||
pathBase.Contains("/wsUser", StringComparison.OrdinalIgnoreCase))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
}
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
ops.Events.OnRedirectToAccessDenied = context =>
{
var path = context.Request.Path.Value ?? string.Empty;
var pathBase = context.Request.PathBase.Value ?? string.Empty;
if (path.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) ||
pathBase.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) ||
path.Contains("/wsUser", StringComparison.OrdinalIgnoreCase) ||
pathBase.Contains("/wsUser", StringComparison.OrdinalIgnoreCase))
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
}
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
```});
#endregion
#region Localización, Memoria y Sesión
builder.Services.AddSingleton<CultureLocalizer>();
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
```javascript
var supportedCultures = new[]
{
new CultureInfo("es-VE"),
new CultureInfo("es-AR"),
new CultureInfo("es-DO"),
new CultureInfo("en")
};
options.DefaultRequestCulture = new RequestCulture("es-VE");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.FallBackToParentCultures = true;
options.FallBackToParentUICultures = true;
options.RequestCultureProviders.Clear();
options.RequestCultureProviders.Add(new CustomRequestCultureProvider(context =>
{
var path = context.Request.Path.Value?.ToLower() ?? string.Empty;
if (path.Contains("/js/") || path.Contains("/css/") || path.Contains("/lib/") || path.Contains("/images/") || path.Contains("/wsSubastas") || path.Contains("/wsUser"))
{
return Task.FromResult<ProviderCultureResult?>(new ProviderCultureResult("es-VE"));
}
return Task.FromResult<ProviderCultureResult?>(null);
}));
options.RequestCultureProviders.Add(new RouteDataRequestCultureProvider());
options.RequestCultureProviders.Add(new CookieRequestCultureProvider());
options.RequestCultureProviders.Add(new AcceptLanguageHeaderRequestCultureProvider());
```});
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddDistributedMemoryCache();
builder.Services.AddMemoryCache();
builder.Services.AddSession(options =>
{
```sql
options.IdleTimeout = TimeSpan.FromHours(8);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
```});
#endregion
#region Políticas de Autorización
builder.Services.AddAuthorization(config =>
{
```javascript
#region Profile
config.AddPolicy("PolicyProfile", policy => policy.RequireRole("OFFICER", "SEGMENTATION", "EXECUTIVE", "TREASURER", "GUEST", "PAYER", "OPERATIONSTREASURER", "ACCOUNTANT", "ADMINISTRATOR", "LEGAL", "MANAGER"));
config.AddPolicy("PolicyProfileChange", policy => policy.RequireRole("ADMINISTRATOR", "LEGAL", "MANAGER"));
config.AddPolicy("PolicyContracts", policy => policy.RequireRole("LEGAL"));
config.AddPolicy("PolicySecurity", policy => policy.RequireRole("ADMINISTRATOR", "LEGAL", "MANAGER"));
#endregion
#region Debtor
config.AddPolicy("RequiredDebtor", policy => policy.RequireRole("DEBTOR"));
config.AddPolicy("PolicyLoadInvoiceDebtor", policy => policy.RequireRole("MANAGER", "PAYER"));
config.AddPolicy("PolicyEditInvoiceDebtor", policy => policy.RequireRole("MANAGER", "PAYER"));
config.AddPolicy("PolicyPostedDebtor", policy => policy.RequireRole("MANAGER", "TREASURER"));
config.AddPolicy("PolicyFinancingDebtor", policy => policy.RequireRole("MANAGER", "TREASURER"));
config.AddPolicy("PolicyConfirmingDebtor", policy => policy.RequireRole("MANAGER", "TREASURER"));
#endregion
#region Supplier
config.AddPolicy("RequiredSupplier", policy => policy.RequireRole("SUPPLIER"));
config.AddPolicy("PolicyPagePublishedSupplier", policy => policy.RequireRole("MANAGER", "TREASURER", "PAYER"));
config.AddPolicy("PolicyAcceptOfferBankSupplier", policy => policy.RequireRole("MANAGER", "TREASURER"));
config.AddPolicy("PolicyPublishedInvoiceSupplier", policy => policy.RequireRole("MANAGER", "TREASURER", "PAYER"));
config.AddPolicy("PolicyOverdueInvoiceSupplier", policy => policy.RequireRole("MANAGER", "TREASURER", "PAYER"));
config.AddPolicy("PolicyLoadInvoiceSupplier", policy => policy.RequireRole("MANAGER", "PAYER"));
config.AddPolicy("PolicyEditInvoiceSupplier", policy => policy.RequireRole("MANAGER", "PAYER"));
config.AddPolicy("PolicyPostedSupplier", policy => policy.RequireRole("MANAGER", "TREASURER"));
config.AddPolicy("PolicyAuctionSupplier", policy => policy.RequireRole("LEGAL", "MANAGER", "TREASURER", "PAYER", "GUEST"));
config.AddPolicy("PolicyCloseAuctionSupplier", policy => policy.RequireRole("MANAGER", "TREASURER"));
#endregion
#region Confirmant
config.AddPolicy("RequiredConfirmant", policy => policy.RequireRole("CONFIRMANT"));
config.AddPolicy("PolicyRegisterDebtorConfirmant", policy => policy.RequireRole("ADMINISTRATOR", "LEGAL", "MANAGER", "EXECUTIVE", "EXTERNAL_BACKOFFICE_CONFIRMANT"));
config.AddPolicy("PolicyVerifyConfirmant", policy => policy.RequireRole("MANAGER", "OFFICER"));
config.AddPolicy("PolicyReviewInvoice", policy => policy.RequireRole("MANAGER", "TREASURER"));
config.AddPolicy("PolicyAssignExecutiveConfirmant", policy => policy.RequireRole("MANAGER", "SEGMENTATION"));
config.AddPolicy("PolicyAssignRiskConfirmant", policy => policy.RequireRole("MANAGER", "EXECUTIVE", "EXTERNAL_BACKOFFICE_CONFIRMANT"));
config.AddPolicy("PolicyAssignFinancingConfirmant", policy => policy.RequireRole("MANAGER", "EXECUTIVE"));
config.AddPolicy("PolicyRateConfirmant", policy => policy.RequireRole("MANAGER", "EXECUTIVE"));
config.AddPolicy("PolicyConfirmInvoiceConfirmant", policy => policy.RequireRole("MANAGER", "EXECUTIVE"));
config.AddPolicy("PolicyBuyInvoiceConfirmant", policy => policy.RequireRole("MANAGER", "TREASURER"));
config.AddPolicy("PolicyPaidInvoiceConfirmant", policy => policy.RequireRole("MANAGER", "OPERATIONSTREASURER"));
config.AddPolicy("PolicyPaidConciliedConfirmant", policy => policy.RequireRole("MANAGER", "OPERATIONSTREASURER"));
config.AddPolicy("PolicyPaidInvoiceOverdueConfirmant", policy => policy.RequireRole("MANAGER", "OPERATIONSTREASURER"));
config.AddPolicy("PolicyExternalTreasurerConfirmant", policy => policy.RequireRole("EXTERNAL_TREASURER"));
config.AddPolicy("PolicyExternalBackofficeConfirmant", policy => policy.RequireRole("EXTERNAL_BACKOFFICE_CONFIRMANT"));
#endregion
#region Factor
config.AddPolicy("RequiredFactor", policy => policy.RequireRole("FACTOR"));
config.AddPolicy("PolicyAuctionFactor", policy => policy.RequireRole("TREASURER", "DPERSON"));
config.AddPolicy("PolicyPaidFactor", policy => policy.RequireRole("TREASURER", "DPERSON", "OPERATIONS"));
#endregion
#region Backoffice
config.AddPolicy("RequiredBackoffice", policy => policy.RequireRole("BACKOFFICE"));
config.AddPolicy("PolicyExternalBackoffice", policy => policy.RequireRole("EXTERNAL_BACKOFFICE"));
#endregion
```});
#endregion
#region MVC, Razor Pages & DB Context
builder.Services.AddControllersWithViews()
```javascript
.AddViewLocalization(o => o.ResourcesPath = "Resources")
.AddModelBindingMessagesLocalizer(builder.Services)
.AddDataAnnotationsLocalization(o =>
{
var type = typeof(ViewResource);
var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName!);
o.DataAnnotationLocalizerProvider = (t, factory) => factory.Create("ViewResource", assemblyName.Name!);
});
```builder.Services.AddSignalR(options =>
{
```sql
options.EnableDetailedErrors = true;
```})
.AddJsonProtocol(options =>
{
```sql
options.PayloadSerializerOptions.PropertyNamingPolicy = null;
```});
builder.Services.AddRazorPages(o =>
{
```java
o.Conventions.Add(new CultureTemplateRouteModelConvention());
o.Conventions.AuthorizeAreaFolder("Profile", "/");
o.Conventions.AuthorizeAreaPage("Profile", "/ActualizarEmpresa", "PolicyProfile");
o.Conventions.AuthorizeAreaPage("Profile", "/MantenimientoUsuarios", "PolicySecurity");
o.Conventions.AuthorizeAreaFolder("Debtor", "/", "RequiredDebtor");
o.Conventions.AuthorizeAreaPage("Debtor", "/CargaMasiva", "PolicyLoadInvoiceDebtor");
o.Conventions.AuthorizeAreaPage("Debtor", "/CargaManual", "PolicyEditInvoiceDebtor");
o.Conventions.AuthorizeAreaPage("Debtor", "/PostularFacturas", "PolicyPostedDebtor");
o.Conventions.AuthorizeAreaPage("Debtor", "/FinanciamientoFacturas", "PolicyFinancingDebtor");
o.Conventions.AuthorizeAreaPage("Debtor", "/ConfirmarFacturas", "PolicyConfirmingDebtor");
o.Conventions.AuthorizeAreaFolder("Confirmant", "/", "RequiredConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/Account/Register", "PolicyRegisterDebtorConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/VerificarDatos", "PolicyVerifyConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/Segmentacion", "PolicyAssignExecutiveConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/EjecutivoCuentas", "PolicyAssignRiskConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/ConfirmarFacturas", "PolicyConfirmInvoiceConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/CompraFactura", "PolicyBuyInvoiceConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/PagoFacturas", "PolicyPaidInvoiceConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/FacturasConciliadas", "PolicyPaidConciliedConfirmant");
o.Conventions.AuthorizeAreaPage("Connfirmant", "/FacturasVencidas", "PolicyPaidInvoiceOverdueConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/SupplierSelection", "PolicyExternalTreasurerConfirmant");
o.Conventions.AuthorizeAreaPage("Confirmant", "/PreassignableInvoices", "PolicyExternalTreasurerConfirmant");
o.Conventions.AuthorizeAreaFolder("Supplier", "/", "RequiredSupplier");
o.Conventions.AuthorizeAreaPage("Supplier", "/PublicarFacturas", "PolicyPagePublishedSupplier");
o.Conventions.AuthorizeAreaPage("Supplier", "/FacturasVencimiento", "PolicyOverdueInvoiceSupplier");
o.Conventions.AuthorizeAreaPage("Supplier", "/MercadoFacturas", "PolicyAuctionSupplier");
o.Conventions.AuthorizeAreaPage("Supplier", "/CierreMercado", "PolicyCloseAuctionSupplier");
o.Conventions.AuthorizeAreaFolder("Factor", "/", "RequiredFactor");
o.Conventions.AuthorizeAreaPage("Factor", "/MercadoFacturas", "PolicyAuctionFactor");
o.Conventions.AuthorizeAreaPage("Factor", "/PagoFacturas", "PolicyPaidFactor");
o.Conventions.AuthorizeAreaFolder("Backoffice", "/", "RequiredBackoffice");
o.Conventions.AuthorizeAreaPage("Backoffice", "/ExternalBackoffice", "PolicyExternalBackoffice");
```}).AddNewtonsoftJson();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
```yaml
options.UseNpgsql(configuration.GetConnectionString("DefaultConnection")));
```#endregion
#region Servicios de Negocio
builder.Services.AddTransient<IEmailSender, AuthMessageSender>();
builder.Services.AddTransient<ISmsSender, AuthMessageSender>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IAuctionService, AuctionService>();
builder.Services.AddScoped<IGlobalService, GlobalService>();
builder.Services.AddScoped<IPeopleService, PeopleService>();
builder.Services.AddScoped<IInvoiceService, InvoiceService>();
builder.Services.AddScoped<IPaymentService, PaymentService>();
builder.Services.AddScoped<IWalletService, WalletService>();
builder.Services.AddSingleton(t => new GraphQLClient(configuration["GraphQlEndpoint"]));
builder.Services.AddScoped<GlobalConsumer>();
builder.Services.AddScoped<AuthConsumer>();
builder.Services.AddScoped<PeopleConsumer>();
builder.Services.AddScoped<InvoiceConsumer>();
builder.Services.AddScoped<AuctionConsumer>();
builder.Services.AddScoped<PaymentConsumer>();
builder.Services.AddScoped<WalletConsumer>();
#endregion
var app = builder.Build();
app.UseStaticFiles();
if (app.Environment.IsDevelopment())
{
```sql
app.UseDeveloperExceptionPage();
```}
else
{
```dockerfile
app.UseExceptionHandler("/Error");
```}
app.UseCors("CorsDevPolicy");
app.UseCookiePolicy();
app.UseSession();
app.UseRouting();
app.UseWhen(context =>
```scala
!context.Request.Path.Value.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) &&
!context.Request.Path.Value.Contains("/wsUser", StringComparison.OrdinalIgnoreCase),
appBuilder =>
{
appBuilder.UseRequestLocalization();
});
```app.UseAuthentication();
app.UseAuthorization();
app.UseWebSockets();
app.Use(async (context, next) =>
{
```vba
if (context.Request.Path.Value?.Contains("wsSubastas") == true && context.Request.Method == "POST")
{
Console.WriteLine("--> LLEGÓ UN POST DE SIGNALR A WSENSUBASTAS");
}
await next();
```});
app.UseEndpoints(endpoints =>
{
```typescript
endpoints.MapControllers();
endpoints.MapRazorPages();
app.MapHub<SocketSubastas>("/wsSubastas", options =>
{
options.Transports = HttpTransportType.WebSockets | HttpTransportType.LongPolling;
});
endpoints.MapHub<SocketUser>("/wsUser").AllowAnonymous().DisableAntiforgery();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
```});
app.Run();
Mi Sokcet de Subasta
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TuFactoring.Services;
using TuFactoringModels;
namespace TuFactoring.WebSocket
{
[AllowAnonymous]
public class SocketSubastas : Hub
{
private readonly IAuctionService _aS;
private readonly SignInManager<User> _signInManager;
public SocketSubastas(IAuctionService aS, SignInManager<User> signInManager)
{
this._aS = aS;
this._signInManager = signInManager;
}
public async Task Publicar(Offert oferta)
{
Publications resultado = new Publications() { Error = new Exception("Invalid offert") };
resultado = await this._aS.OfferPublication(oferta, oferta.Token);
if (resultado.Errors != null || resultado.Error != null)
{
await Clients.Caller.SendAsync("Publications", resultado);
return;
}
resultado.Bids = new List<Bids>() { new Bids() { Factor = new People() { Id = oferta.Factor_id } } };
if (oferta.Factor_id == resultado.Bids[0].Factor_id)
{
resultado.isOffered = true;
}
await Clients.Group(resultado.Invoice.Country.Value + "").SendAsync("Publications", resultado);
}
public async Task Cierre(string identificador)
{
await Clients.Group(identificador).SendAsync("Cierre", "close");
}
public async Task NotifyFactor(List<Notification> notification, string identificador)
{
await Clients.Group(identificador).SendAsync("Notification", notification);
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
}
public async Task loginGroup(string group)
{
if (String.IsNullOrEmpty(group)) return;
await Groups.AddToGroupAsync(Context.ConnectionId, group);
}
}
}
Mi resourceConection que es el gestiona la conection dinamicamente depnediendo del socket a llamar
let connectionSignalRActual = null;
let functionPostConnectionActual = function () { };
let rutaActual = "";
async function startSignalrConnection(ruta, functionPostConnection) {
```javascript
console.log("SignalR Version:", signalR.VERSION);
const rutaAbsoluta = ruta.startsWith('/') ? ruta : `/${ruta}`;
console.log('Conectando a la ruta de SignalR: ' + rutaAbsoluta);
const connection = new signalR.HubConnectionBuilder()
.withUrl(rutaAbsoluta, {
transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling,
withCredentials: true
})
.configureLogging(signalR.LogLevel.Debug)
.withAutomaticReconnect()
.build();
try {
await connection.start();
console.log("SignalR conectado con éxito a:", rutaAbsoluta);
rutaActual = rutaAbsoluta;
connectionSignalRActual = connection;
if (typeof postConnectAux !== "undefined" && functionPostConnection !== postConnectAux) {
functionPostConnectionActual = functionPostConnection;
}
if (typeof functionPostConnection === "function") {
functionPostConnection(connection);
}
connection.onreconnected(() => {
console.log("SignalR reconectado automáticamente.");
if (typeof functionPostConnectionActual === "function") {
functionPostConnectionActual(connection);
}
});
if (typeof closeSignalR === "function") {
connection.onclose(closeSignalR);
}
} catch (err) {
console.error("Error crítico en SignalR al conectar:", err);
}
```}