Reputation: 822
I have a Blazor wasm application that uses SignalR to send messages to clients. When sending message to all clients, everything works fine. In order to send messages to particular clients, I wanted to create a group for each client:
public override async Task OnConnectedAsync()
{
//enter code here to keep track of connected clients
var userName = Context.User.FindFirst(ClaimTypes.NameIdentifier); // get the username of the connected user
await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userName}");
await base.OnConnectedAsync();
}
So in the Groups.AddToGroupAsync, I want to add the ConnectionId and userName. However, when a connection is created userName is always null. How should I get a unique userName here? All my users are registered using .NET Core Identity and IdnetityServer. I also tried the [Authorize] attribute on this method but userName is still null.
If I use the [Authorize] attribute on the hub class, I get an UnAuthorized 401 error when connecting to the hub from the client:
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/broadcaster"))
.Build();
...
await hubConnection.StartAsync(); //returns 401 error
Here is the startup.cs:
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PakPx1.Server.Data;
using PakPx1.Server.Models;
using System;
using Syncfusion.Blazor;
using System.Linq;
using System.Threading.Tasks;
using PakPx1.Server.Hubs;
using PakPx1.Shared.Models;
using PakPx1.Client;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using PakPx1.Server.Utils;
using Microsoft.AspNetCore.SignalR;
namespace PakPx1.Server
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
//services.AddDbContext<ApplicationDbContext>(options =>
// options.UseSqlServer(
// Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddSyncfusionBlazor();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection"), new MySqlServerVersion(new Version(5, 6, 48))));
services.AddOptions();
services.AddControllers();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddHttpContextAccessor();
services.AddAuthorization();
//services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
services.AddControllersWithViews();
services.AddRazorPages();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = false;
options.Password.RequiredLength = 4;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
});
services.AddServerSideBlazor();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
services.AddMvc().AddMvcOptions(options =>
{
options.EnableEndpointRouting = false;
});
services.AddMvcCore(options => options.OutputFormatters.Add(new XmlSerializerOutputFormatter()));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddMvc().AddXmlDataContractSerializerFormatters();
services.AddSignalR();
//services.AddCors(options =>
//{
// options.AddPolicy("EnableCORS", builder =>
// {
// builder.AllowAnyOrigin().AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials().Build();
// });
//});
//services.AddMvc(option => option.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapHub<Broadcaster>("/broadcaster");
endpoints.MapFallbackToFile("index.html");
});
CreateRoles(serviceProvider).Wait();
}
private async Task CreateRoles(IServiceProvider serviceProvider)
{
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
string[] roleNames = { "Admin", "Business Manager", "Client", "Settlement Manager", "VOTClient" };
IdentityResult roleResult;
foreach (var roleName in roleNames)
{
var roleExist = await RoleManager.RoleExistsAsync(roleName);
if (!roleExist)
{
roleResult = await RoleManager.CreateAsync(new IdentityRole(roleName));
}
}
}
}
}
Upvotes: 2
Views: 3009
Reputation: 822
Found the solution. Turns out the following change is required when creating the hub connection:
From this:
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/broadcaster"))
.Build();
To this:
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/broadcaster"), options =>
{
options.AccessTokenProvider = async () =>
{
var accessTokenResult = await AccessTokenProvider.RequestAccessToken();
accessTokenResult.TryGetToken(out var accessToken);
return accessToken.Value;
};
})
.Build();
This SO post was helpful: SignalR Hub Authorization for Blazor WebAssembly with Identity.
Once you do this, the [Authorize] attribute on the hub class works and you can get the NameIdentifier claim that is used by SignalR.
Upvotes: 4
Reputation: 733
your simple answer is send to specific user with id:
Clients.Client(connectionId).SendAsync
but first of all you need to save all connection id, also remove id when user disconnect. in your hub controller override these methods:
public static HashSet<UserInfoDto> Users = new HashSet<UserInfoDto>();
public override Task OnConnectedAsync()
{
Users.Add(new UserInfoDto() { ConnectionId = Context.ConnectionId });
return base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var usr = Users.First(x => x.ConnectionId == Context.ConnectionId);
Users.Remove(usr);
if (!string.IsNullOrEmpty(usr.Name))
{
await UserContactListUpdate();
}
await base.OnDisconnectedAsync(exception);
}
next you need to store additional data after login, start or other state, for example we need to add login information after user submit info, we update info for this user:
public async Task Login(string connectionId, string name, string imageId)
{
var user = Users.First(x => x.ConnectionId == connectionId);
if (string.IsNullOrEmpty(imageId))
{
var path = Path.Combine(environment.ContentRootPath, "wwwroot", "images");
var files = Directory.GetFiles(path, "*.png");
var rnd = new Random();
var index = rnd.Next(1, files.Length);
imageId = Path.GetFileNameWithoutExtension(files[index]);
}
user.Name = name;
user.ImageUrl = imageId;
await Clients.Client(connectionId).SendAsync("LoginSucess", user);
await UserContactListUpdate();
}
I wrote an example chat server application in https://github.com/mahdiit/ChatServer
Upvotes: 3