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