1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Anuncie Aqui
    Anuncie aqui você Também: fdantas@4each.com.br

SignalR doesn't connect when [Authorize] attribute added to hub

Discussão em 'Angular' iniciado por Eis Karlsson, Agosto 3, 2025.

  1. Eis Karlsson

    Eis Karlsson Guest

    I am trying to connect to SignalR. I am using .NET WebApi and Angular. I have tried almost everything that came to mind, including some chats with AI. If I remove the Authorize attribute while the application is running, it seems to work, however Context.User.IsAuthorized is false.

    If I close Visual Studio and rest with the Authorize attribute present, the problem persists.

    [​IMG]

    Here is my Hub:

    using Microsoft.AspNetCore.SignalR;
    using System.Security.Claims;
    using System.Text.RegularExpressions;
    using limbo.dating.Server.DTO.Messages;
    using Microsoft.AspNetCore.Authorization;

    namespace limbo.dating.Server.Hubs
    {
    [Authorize]
    public class FriendHub : Hub
    {
    private readonly ILogger<FriendHub> _logger;


    public FriendHub(ILogger<FriendHub> logger)
    {
    _logger = logger;
    }

    public async Task JoinUserGroup()
    {
    var userId = Context.User?.FindFirstValue(ClaimTypes.NameIdentifier);
    if (userId != null)
    {
    await Groups.AddToGroupAsync(Context.ConnectionId, userId);
    _logger.LogInformation($"User {userId} joined their notification group.");
    }
    }

    public override Task OnConnectedAsync()
    {
    Console.WriteLine("SignalR client connected: " + Context.ConnectionId);
    return base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
    var userId = Context.User?.FindFirstValue(ClaimTypes.NameIdentifier);
    if (userId != null)
    {
    await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId);
    }
    await base.OnDisconnectedAsync(exception);
    }

    //Messaging methods
    /// <summary>
    /// Send a message to a specific friend
    /// </summary>
    public async Task SendMessageToFriend(CreateMessageDto createMessageDto)
    {
    //TODO: senderId is null
    var senderId = Context.UserIdentifier;
    if (string.IsNullOrEmpty(senderId)) return;

    await Clients.Group(createMessageDto.RecipientId).SendAsync("ReceiveMessage", new
    {
    MessageId = createMessageDto.MessageId,
    SenderId = senderId,
    RecipientId = createMessageDto.RecipientId,
    Content = createMessageDto.Content,
    Timestamp = DateTime.UtcNow,
    IsRead = false
    });

    // Also send to sender's other devices
    await Clients.Group(senderId).SendAsync("MessageSentConfirmation", new
    {
    MessageId = createMessageDto.MessageId,
    RecipientId = createMessageDto.RecipientId,
    Timestamp = DateTime.UtcNow
    });
    }

    /// <summary>
    /// Notify when a message has been read
    /// </summary>
    public async Task NotifyMessageRead(string messageId, string senderId)
    {
    await Clients.Group(senderId).SendAsync("MessageRead", new
    {
    MessageId = messageId,
    ReadAt = DateTime.UtcNow
    });
    }

    /// <summary>
    /// Send typing indicator to a friend
    /// </summary>
    public async Task SendTypingIndicator(string recipientId, bool isTyping)
    {
    var senderId = Context.UserIdentifier;
    if (string.IsNullOrEmpty(senderId)) return;

    await Clients.Group(recipientId).SendAsync("FriendIsTyping", new
    {
    SenderId = senderId,
    IsTyping = isTyping
    });
    }

    /// <summary>
    /// Notify when messages are being loaded (for UI feedback)
    /// </summary>
    public async Task NotifyLoadingMessages(string recipientId)
    {
    await Clients.Group(recipientId).SendAsync("FriendIsLoadingMessages");
    }

    /// <summary>
    /// Notify when new messages are available in a conversation
    /// </summary>
    public async Task NotifyNewMessagesAvailable(string friendId)
    {
    var userId = Context.UserIdentifier;
    await Clients.Group(friendId).SendAsync("NewMessagesAvailable", userId);
    }
    }
    }

    Here is my Program.cs:

    //https://github.com/MoonriseSoftwareCalifornia/AspNetCore.Identity.CosmosDb
    //https://github.com/MoonriseSoftwareCalifornia/AspNetCore.Identity.CosmosDb/blob/main/AspNetCore.Identity.CosmosDb.Example/Areas/Identity/Pages/Account/Login.cshtml.cs

    using AspNetCore.Identity.CosmosDb;
    using AspNetCore.Identity.CosmosDb.Containers;
    using AspNetCore.Identity.CosmosDb.Extensions;
    using limbo.dating.Server;
    using limbo.dating.Server.Controllers;
    using limbo.dating.Server.Data;
    using limbo.dating.Server.Data.AspNetCore.Identity.CosmosDb.Example.Data;
    using limbo.dating.Server.Hubs;
    using limbo.dating.Server.Models;
    using limbo.dating.Server.Models.AngularCoreCosmos.Models;
    using limbo.dating.Server.Services;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Azure;
    using Microsoft.IdentityModel.Tokens;
    using System.Security.Claims;
    using System.Text;
    using limbo.dating.Server.Models.limbo.dating.Server.Models;

    var builder = WebApplication.CreateBuilder(args);

    // The Cosmos connection string
    var connectionString = builder.Configuration.GetConnectionString("ApplicationDbContextConnection");

    var cosmosAccountKey = builder.Configuration.GetValue<string>("CosmosAccountKey");

    // Name of the Cosmos database to use
    var cosmosIdentityDbName = builder.Configuration.GetValue<string>("CosmosIdentityDbName");

    // If this is set, the Cosmos identity provider will:
    // 1. Create the database if it does not already exist.
    // 2. Create the required containers if they do not already exist.
    // IMPORTANT: Remove this setting if after first run. It will improve startup performance.
    var setupCosmosDb = builder.Configuration.GetValue<string>("SetupCosmosDb");

    // If the following is set, it will create the Cosmos database and
    // required containers.
    if (bool.TryParse(setupCosmosDb, out var setup) && setup)
    {
    var builder1 = new DbContextOptionsBuilder<LimboDatingDbContext>();
    builder1.UseCosmos(connectionString, cosmosIdentityDbName);

    await using (var dbContext = new LimboDatingDbContext(builder1.Options))
    {
    await dbContext.Database.EnsureCreatedAsync();
    }
    }

    builder.Services.AddDbContext<LimboDatingDbContext>(options =>
    options.UseCosmos(connectionString: connectionString, databaseName: cosmosIdentityDbName));

    builder.Services.AddCosmosIdentity<LimboDatingDbContext, LimboUser, IdentityRole, string>(
    options =>
    {
    options.SignIn.RequireConfirmedAccount = true;
    options.User.RequireUniqueEmail = true;
    options.Password.RequiredLength = 3;
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = false;
    options.Lockout.AllowedForNewUsers = true;

    }
    // Always a good idea :)

    )
    //.AddDefaultUI() // Use this if Identity Scaffolding is in use
    .AddRoles<IdentityRole>() // be able to add roles
    .AddRoleManager<RoleManager<IdentityRole>>() // be able to make use of RoleManager
    .AddEntityFrameworkStores<LimboDatingDbContext>() // providing our context
    .AddSignInManager<SignInManager<LimboUser>>() // make use of Signin manager
    .AddUserManager<UserManager<LimboUser>>() // make use of UserManager to create users
    .AddEntityFrameworkStores<LimboDatingDbContext>() // providing our context
    .AddSignInManager<SignInManager<LimboUser>>() // make use of Signin manager
    .AddUserManager<UserManager<LimboUser>>() // make use of UserManager to create users
    .AddDefaultTokenProviders();

    builder.Services.AddAzureClients(clientBuilder =>
    {
    clientBuilder.AddBlobServiceClient(builder.Configuration.GetConnectionString("AzureBlobStorage"));
    });

    builder.Services.AddOptions<ImagesController.AzureBlobStorageSettings>()
    .BindConfiguration(ImagesController.AzureBlobStorageSettings.SectionName)
    .ValidateDataAnnotations();

    // be able to inject JWTService class inside our Controllers
    builder.Services.AddScoped<JWTService>();
    builder.Services.AddScoped<EmailService>();
    builder.Services.AddScoped<ContextSeedService>();

    // be able to authenticate users using JWT
    builder.Services.AddAuthentication(options =>
    {
    options.DefaultScheme = "JWT_OR_COOKIES"; // Custom policy to check both
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddCookie("Cookies", options =>
    {
    options.LoginPath = "/Account/Login";
    options.AccessDeniedPath = "/Account/AccessDenied";
    options.Events.OnRedirectToLogin = context =>
    {
    // Skip redirect for API calls
    if (context.Request.Path.StartsWithSegments("/api"))
    {
    context.Response.StatusCode = 401;
    return Task.CompletedTask;
    }
    context.Response.Redirect(context.RedirectUri);
    return Task.CompletedTask;
    };
    })
    .AddJwtBearer(options =>
    {
    options.TokenValidationParameters = new TokenValidationParameters
    {
    // validate the token based on the key we have provided inside appsettings.development.json JWT:Key
    ValidateIssuerSigningKey = true,
    // the issuer singning key based on JWT:Key
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"])),
    // the issuer which in here is the api project url we are using
    ValidIssuer = builder.Configuration["JWT:Issuer"],
    // validate the issuer (who ever is issuing the JWT)
    ValidateIssuer = true,
    // don't validate audience (angular side)
    ValidateAudience = false,
    ValidateLifetime = true,
    ClockSkew = TimeSpan.Zero
    };

    options.Events = new JwtBearerEvents
    {
    OnMessageReceived = context =>
    {
    var accessToken = context.Request.Query["access_token"];
    var path = context.HttpContext.Request.Path;

    // Only set the token if the request is for the SignalR hub
    if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/friendHub"))
    {
    context.Token = accessToken;
    }

    return Task.CompletedTask;
    }

    };
    })
    .AddPolicyScheme("JWT_OR_COOKIES", "JWT_OR_COOKIES", options =>
    {
    options.ForwardDefaultSelector = context =>
    {
    string authorization = context.Request.Headers.Authorization;
    if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
    return "Bearer";

    return "Cookies";
    };
    });

    builder.Services.AddHttpClient();


    builder.Services.AddCors(options =>
    {
    options.AddPolicy(name: "AllowAll",
    builder =>
    {
    builder.WithOrigins("https://localhost:4200")
    .AllowAnyHeader()
    //.AllowAnyOrigin()
    .AllowAnyMethod()
    .AllowCredentials()
    .WithExposedHeaders("WWW-Authenticate")
    .SetPreflightMaxAge(TimeSpan.FromSeconds(86400));
    });
    });

    builder.Services.Configure<ApiBehaviorOptions>(options =>
    {
    options.InvalidModelStateResponseFactory = actionContext =>
    {
    var errors = actionContext.ModelState
    .Where(x => x.Value.Errors.Count > 0)
    .SelectMany(x => x.Value.Errors)
    .Select(x => x.ErrorMessage).ToArray();

    var toReturn = new
    {
    Errors = errors
    };

    return new BadRequestObjectResult(toReturn);
    };
    });

    builder.Services.AddAuthorization(opt =>
    {
    opt.AddPolicy("AdminPolicy", policy => policy.RequireRole("Admin"));
    opt.AddPolicy("ManagerPolicy", policy => policy.RequireRole("Manager"));
    opt.AddPolicy("PlayerPolicy", policy => policy.RequireRole("Player"));

    opt.AddPolicy("AdminOrManagerPolicy", policy => policy.RequireRole("Admin", "Manager"));
    opt.AddPolicy("AdminAndManagerPolicy", policy => policy.RequireRole("Admin").RequireRole("Manager"));
    opt.AddPolicy("AllRolePolicy", policy => policy.RequireRole("Admin", "Manager", "Player"));

    opt.AddPolicy("AdminEmailPolicy", policy => policy.RequireClaim(ClaimTypes.Email, "admin@example.com"));
    opt.AddPolicy("MillerSurnamePolicy", policy => policy.RequireClaim(ClaimTypes.Surname, "miller"));
    opt.AddPolicy("ManagerEmailAndWilsonSurnamePolicy", policy => policy.RequireClaim(ClaimTypes.Surname, "wilson")
    .RequireClaim(ClaimTypes.Email, "manager@example.com"));
    opt.AddPolicy("VIPPolicy", policy => policy.RequireAssertion(context => SD.VIPPolicy(context)));
    });

    // Add services to the container.

    builder.Services.AddControllers();
    // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
    builder.Services.AddOpenApi();

    // Register the repository with a synchronous factory that blocks (only during startup)
    builder.Services.AddSingleton<IDocumentDBRepository<User>>(provider =>
    {
    var config = provider.GetRequiredService<IConfiguration>();

    // Block synchronously during app startup (acceptable for initialization)
    return DocumentDBRepository<User>.CreateAsync(connectionString, cosmosAccountKey, cosmosIdentityDbName)
    .GetAwaiter()
    .GetResult();
    });

    builder.Services.AddSingleton<IDocumentDBRepository<Friendship>>(provider =>
    {
    var config = provider.GetRequiredService<IConfiguration>();

    // Block synchronously during app startup (acceptable for initialization)
    return DocumentDBRepository<Friendship>.CreateAsync(connectionString, cosmosAccountKey, cosmosIdentityDbName)
    .GetAwaiter()
    .GetResult();
    });

    builder.Services.AddSingleton<IDocumentDBRepository<ChatMessage>>(provider =>
    {
    var config = provider.GetRequiredService<IConfiguration>();

    // Block synchronously during app startup (acceptable for initialization)
    return DocumentDBRepository<ChatMessage>.CreateAsync(connectionString, cosmosAccountKey, cosmosIdentityDbName)
    .GetAwaiter()
    .GetResult();
    });

    builder.Services.AddSignalR(hubOptions => {
    // Timeout settings
    hubOptions.ClientTimeoutInterval = TimeSpan.FromMinutes(2);
    hubOptions.HandshakeTimeout = TimeSpan.FromSeconds(15);

    // Keep-alive settings
    hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(15);

    // Additional options
    hubOptions.EnableDetailedErrors = true; // For debugging
    hubOptions.MaximumParallelInvocationsPerClient = 1;
    });

    var app = builder.Build();



    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
    app.MapOpenApi();
    }

    app.UseHttpsRedirection();

    //app.MapControllers();
    //app.MapFallbackToController("Index", "Fallback");

    app.UseDefaultFiles();
    app.UseStaticFiles();
    app.MapStaticAssets();



    app.UseCors("AllowAll");

    app.UseWebSockets();

    // adding UseAuthentication into our pipeline and this should come before UseAuthorization
    // Authentication verifies the identity of a user or service, and authorization determines their access rights.
    app.UseAuthentication();
    app.UseAuthorization();

    // Add to your app configuration (before MapControllers)
    app.MapHub<FriendHub>("/friendHub");
    app.MapControllers();

    app.MapGet("/test", () => "Server is running.");

    app.MapFallbackToFile("/index.html");



    #region ContextSeed
    using var scope = app.Services.CreateScope();
    try
    {
    var contextSeedService = scope.ServiceProvider.GetService<ContextSeedService>();
    await contextSeedService.InitializeContextAsync();
    }
    catch (Exception ex)
    {
    var logger = scope.ServiceProvider.GetService<ILogger<Program>>();
    logger.LogError(ex.Message, "Failed to initialize and seed the database");
    }
    #endregion

    app.Run();

    And here is my signalr.service.ts:

    import { Injectable } from '@angular/core';
    import * as signalR from '@microsoft/signalr';
    import { Subject, Observable, BehaviorSubject } from 'rxjs';
    import { environment } from 'src/environments/environment';
    import { AccountService } from '../account/account.service';
    import { MessageDto } from '../models/messageDto';

    @Injectable({ providedIn: 'root' })
    export class SignalRService {
    private hubURL = "https://localhost:5210/friendHub";
    public hubConnection!: signalR.HubConnection;
    private connectionState = new BehaviorSubject<signalR.HubConnectionState>(signalR.HubConnectionState.Disconnected);
    private notificationHandlers = new Map<string, (...args: any[]) => void>();
    private messageQueue: { methodName: string, args: any[] }[] = [];
    private isExplicitlyDisconnected = false;

    public connectionState$ = this.connectionState.asObservable();

    constructor(private authService: AccountService) {
    this.createConnection();
    this.startConnection();
    }

    private createConnection(): void {
    this.hubConnection = new signalR.HubConnectionBuilder()
    //.withUrl(`${this.hubURL}?access_token=${this.authService.getJWT() ?? ''}`)
    .withUrl(this.hubURL, {
    accessTokenFactory: () => {
    const token = this.authService.getJWT();
    console.log('Using token:', token); // Debug logging
    return token ?? '';
    },
    skipNegotiation: true, // Keep negotiation enabled for now
    transport: signalR.HttpTransportType.WebSockets
    })
    // .withUrl(this.hubURL, {
    // accessTokenFactory: () => this.authService.getJWT() ?? '',
    // skipNegotiation: true,
    // transport: signalR.HttpTransportType.WebSockets
    // })
    .withAutomaticReconnect({
    nextRetryDelayInMilliseconds: retryContext => {
    return Math.min(retryContext.previousRetryCount * 2000, 10000);
    }
    })
    .configureLogging(signalR.LogLevel.Warning)
    .build();

    this.registerConnectionEvents();
    }

    private registerConnectionEvents(): void {
    this.hubConnection.onreconnecting(error => {
    console.log(`SignalR reconnecting due to ${error?.message}`);
    this.connectionState.next(signalR.HubConnectionState.Reconnecting);
    });

    this.hubConnection.onreconnected(connectionId => {
    console.log(`SignalR reconnected. New connection ID: ${connectionId}`);
    this.connectionState.next(signalR.HubConnectionState.Connected);
    this.processMessageQueue();
    });

    this.hubConnection.onclose(error => {
    console.log(`SignalR connection closed. ${error?.message}`);
    this.connectionState.next(signalR.HubConnectionState.Disconnected);
    if (!this.isExplicitlyDisconnected && error) {
    setTimeout(() => this.startConnection(), 5000);
    }
    });
    }

    public async startConnection(): Promise<void> {
    if (this.hubConnection?.state === signalR.HubConnectionState.Disconnected) {
    this.isExplicitlyDisconnected = false;
    try {
    await this.hubConnection.start();
    console.log('SignalR Connected');
    this.connectionState.next(signalR.HubConnectionState.Connected);
    this.joinUserGroup();
    this.processMessageQueue();
    } catch (err) {
    console.error('Error starting SignalR connection:', err);
    this.connectionState.next(signalR.HubConnectionState.Disconnected);
    setTimeout(() => this.startConnection(), 5000);
    }
    }
    }

    private async processMessageQueue(): Promise<void> {
    while (this.messageQueue.length > 0 &&
    this.hubConnection.state === signalR.HubConnectionState.Connected) {
    const message = this.messageQueue.shift();
    try {
    await this.hubConnection.invoke(message!.methodName, ...message!.args);
    } catch (err) {
    console.error(`Error processing queued message ${message!.methodName}:`, err);
    this.messageQueue.unshift(message!); // Put it back if failed
    break;
    }
    }
    }

    private joinUserGroup(): void {
    this.invokeWithQueue('JoinUserGroup').catch(err =>
    console.error('Error joining user group:', err));
    }

    public async invokeWithQueue(methodName: string, ...args: any[]): Promise<any> {
    if (this.hubConnection.state === signalR.HubConnectionState.Connected) {
    try {
    return await this.hubConnection.invoke(methodName, ...args);
    } catch (err) {
    console.error(`Error invoking ${methodName}:`, err);
    throw err;
    }
    } else {
    console.log(`Queueing ${methodName} (connection state: ${this.hubConnection.state})`);
    this.messageQueue.push({ methodName, args });
    if (this.hubConnection.state === signalR.HubConnectionState.Disconnected) {
    await this.startConnection();
    }
    return Promise.resolve();
    }
    }

    public on<T>(methodName: string, callback: (data: T) => void): void {
    if (this.notificationHandlers.has(methodName)) {
    const handler = this.notificationHandlers.get(methodName);
    if (handler) {
    this.hubConnection.off(methodName, handler);
    }
    }

    this.notificationHandlers.set(methodName, callback);
    this.hubConnection.on(methodName, callback);
    }

    public off(methodName: string): void {
    const handler = this.notificationHandlers.get(methodName);
    if (handler) {
    this.hubConnection.off(methodName, handler);
    this.notificationHandlers.delete(methodName);
    }
    }

    public async stopConnection(): Promise<void> {
    this.isExplicitlyDisconnected = true;
    try {
    await this.hubConnection.stop();
    } catch (err) {
    console.error('Error stopping SignalR connection:', err);
    }
    }

    addFriendRequestListener(callback: (request: { fromUserName: string }) => void) {
    this.on('ReceiveFriendRequest', callback);
    }

    notifyMessageRead(messageId: string): void {
    this.invokeWithQueue('MarkAsRead', messageId)
    .catch(err => console.error('Error notifying message read:', err));
    }

    sendMessage(messageDto: MessageDto): void {
    this.invokeWithQueue('SendMessageToFriend', messageDto)
    .catch(err => console.error('Error sending message:', err));
    }
    }

    Continue reading...

Compartilhe esta Página