How to send a SignalR message to a user? - asp.net-core

How does the client authorize to send a message to the user?
Sending from the controller
hubContext.Clients.User(User.Identity.Name).SendAsync();
At the moment the message is not sent. Do I need to add something in OnConnection ()? Or does SignalR have a ready-made mapping mechanism for ConnectionId and User.Identity.Name?
That's how I implemented it at the moment, but it seems to me not quite right. The question is how to make the same standard tools?
public static class HubConnections
{
public static Dictionary<string, List<string>> Users = new Dictionary<string, List<string>>();
public static List<string> GetUserId(string name)
{
return Users[name];
}
}
public class GameHub : Hub
{
public override Task OnConnectedAsync()
{
if (Context.User.Identity.IsAuthenticated
&& HubConnections.Users.ContainsKey(Context.User.Identity.Name)
&& !HubConnections.Users[Context.User.Identity.Name].Contains(Context.ConnectionId))
HubConnections.Users[Context.User.Identity.Name].Add(Context.ConnectionId);
else
HubConnections.Users.Add(Context.User.Identity.Name, new List<string> { Context.ConnectionId });
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
if (Context.User.Identity.IsAuthenticated) HubConnections.Users.Remove(Context.User.Identity.Name);
return base.OnDisconnectedAsync(exception);
}
}
As I said above, I tried just like this, and it does not work
hubContext.Clients.User(User.Identity.Name).SendAsync();

Was chasing the same issue and got the solution from https://github.com/aspnet/SignalR/issues/2498
One needs to set the NameIdentifier claim. That is the one checked by SignalR instead of the Name claim which I assumed. I set the NameIdentifier claim and I got my non-hub class to send a notification to a specific user.

The claim that signalR is using to identify the user can be changed. It is important to ensure that this claim has unique values.
Documentation says to setup a custom UserIdProvider like this:
public class NameUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
return connection.User?.Identity?.Name;
}
}
Add then add it to services:
public void ConfigureServices(IServiceCollection services)
{
// ... other services ...
services.AddSignalR();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}
The snippets are taken from official documentation:
https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1#use-claims-to-customize-identity-handling

There's a client-side component. You must reference the SignalR JS file, create a connection and then subscribe to a particular message from the server. Only then will sending that message actually do something.
<script src="~/lib/signalr/signalr.js"></script>
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("/gameHub")
.configureLogging(signalR.LogLevel.Information)
.build();
connection.on("Foo", (data) => {
// do something
});
connection.start().catch(err => console.error(err.toString()));
</script>
The above will then cause the client to run the function defined for "Foo" above whenever the server sends a "Foo" message like:
hubContext.Clients.User(User.Identity.Name).SendAsync("Foo", data);

You are using the Users as a store for the connection id. So, for each username, you can send the message to each of the client connections you have stored for that user. Something like this:
public void SendMessage(string username, object data)
{
var connections = HubConnections.Users[Context.User.Identity.Name];
foreach(var id in connections)
{
Clients.client(id).SendAsync("Foo", data);
}
}

Related

How to send message to specific user by only userid in Azure signalr?

Hi I am working in azure SignalR. I have made setup to send message to specif user by using combination of connectionid and userid and this works perfect for me.
public class UserConnectionManager : IUserConnectionManager
private static Dictionary<string, List<string>> userConnectionMap = new Dictionary<string, List<string>>();
private static string userConnectionMapLocker = string.Empty;
public void KeepUserConnection(string userId, string connectionId)
{
lock (userConnectionMapLocker)
{
if (!userConnectionMap.ContainsKey(userId))
{
userConnectionMap[userId] = new List<string>();
}
userConnectionMap[userId].Add(connectionId);
}
}
For me this implementation works perfect. Now I have deployed my app in kubernetes with two pods so whats happening is list I used to store connectionids is static and kind of session behavior. So its not working with two pods.
[Route("Push")]
[HttpPost]
public async Task<ActionResult> Push(Failure Failure)
{
var connections = _userConnectionManager.GetUserConnections(Failure.UserId);
if (connections != null && connections.Count > 0)
{
foreach (var connectionId in connections)
{
await _notificationUserHubContext.Clients.Client(connectionId).SendAsync("MyMessage", Failure);
}
}
return ok();
}
So I do not want to make combination of connectioid and userid. I want to just push message to user by only userid. However I tried below approach
await
_notificationUserHubContext.Clients.User(Failure.UserId).SendAsync("MyMessage",
mapDataResponseSuccess);
This piece of code is not pushing messages to specified userid. Can someone help me to send message to specific user by only UserId? Any help would be appreciated. Thank you

.NET Core Identity Framework and Telegram Login Widget with no database

I am making a Blazor Server app, which is tied to my Telegram bot. I want to add the ability for the user to login using Telegram Login Widget. I have no plans to add login/password authentication and I therefore don't see any reason to use the database to store anything login-related other than the Telegram User ID.
All of the samples imply using the login-password model along with the database, somewhat like this:
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<AppDbContext>();
Inevitable, this line appears in all of the samples: services.AddEntityFrameworkStores<AppDbContext>();
Here's my question: how do I just put the user's data (after checking the info from Telegram) into app's context, without storing anything in the database? Or if I'm forced to, where do I change the database scheme? Maybe I don't even need to use the Identity framework for this? All I want is for all the pages to have the info about the user, and the authentication happens on Telegram's side, I just get all the info in response and check the hash with my private key. All I want to do after that is put that model into app's context, I'm not even sure I plan on storing the cookie for the user.
To be clear: I already know how to get info from Telegram and check the hash, let's assume after executing some code on a page I already have some User model with some filled out fields
In the end, this is how I did it. While not ideal, this works for me. However, I'd love to get some clarifications from someone, specifically on IUserStore stuff.
I've added Blazored SessionStorage as a dependency to the project
I've registered my own implementations of AuthenticationStateProvider, IUserStore and IRoleStore in Startup.cs like this:
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
services.AddTransient<IUserStore<User>, CustomUserStore>();
services.AddTransient<IRoleStore<Role>, CustomRoleStore>();
The first line is the most important one. Implementations of IUserStore and IRoleStore don't really matter, but it seems like I have to register them for Identity framework to work, even though I won't use them. All of the methods in my "implementation" are literally just throw new NotImplementedException(); and it still works, it just needs them to exist for the UserManager somewhere deep down, I guess? I'm still a little unclear on that.
My CustomAuthenticationStateProvider looks like this:
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly ISessionStorageService _sessionStorage;
private readonly ILogger _logger;
private readonly AuthenticationState _anonymous = new(new ClaimsPrincipal(new ClaimsIdentity()));
public CustomAuthenticationStateProvider(
ILoggerFactory loggerFactory,
ISessionStorageService sessionStorage,
IConfiguration configuration) : base(loggerFactory)
{
_logger = loggerFactory.CreateLogger<CustomAuthenticationStateProvider>();
_sessionStorage = sessionStorage;
// setting up HMACSHA256 for checking user data from Telegram widget
...
}
private bool IsAuthDataValid(User user)
{
// validating user data with bot token as the secret key
...
}
public AuthenticationState AuthenticateUser(User user)
{
if (!IsAuthDataValid(user))
{
return _anonymous;
}
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Sid, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.FirstName),
new Claim("Username", user.Username),
new Claim("Avatar", user.PhotoUrl),
new Claim("AuthDate", user.AuthDate.ToString()),
}, "Telegram");
var principal = new ClaimsPrincipal(identity);
var authState = new AuthenticationState(principal);
base.SetAuthenticationState(Task.FromResult(authState));
_sessionStorage.SetItemAsync("user", user);
return authState;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var state = await base.GetAuthenticationStateAsync();
if (state.User.Identity.IsAuthenticated)
{
return state;
}
try
{
var user = await _sessionStorage.GetItemAsync<User>("user");
return AuthenticateUser(user);
}
// this happens on pre-render
catch (InvalidOperationException)
{
return _anonymous;
}
}
public void Logout()
{
_sessionStorage.RemoveItemAsync("user");
base.SetAuthenticationState(Task.FromResult(_anonymous));
}
protected override async Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState,
CancellationToken cancellationToken)
{
try
{
var user = await _sessionStorage.GetItemAsync<User>("user");
return user != null && IsAuthDataValid(user);
}
// this shouldn't happen, but just in case
catch (InvalidOperationException)
{
return false;
}
}
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromHours(1);
}
In my Login Blazor page I inject the CustomAuthenticationStateProvider like this:
#inject AuthenticationStateProvider _authenticationStateProvider
And finally, after getting data from the Telegram widget, I call the AuthenticateUser method:
((CustomAuthenticationStateProvider)_authenticationStateProvider).AuthenticateUser(user);
Note, that I have to cast AuthenticationStateProvider to CustomAuthenticationStateProvider to get exactly the same instance as AuthorizedView would.
Another important point is that AuthenticateUser method contains call to SessionStorage, which is available later in the lifecycle of the page, when OnAfterRender has completed, so it will throw an exception, if called earlier.

Transferring objects across service provider scopes

I have a CorrelationIdMiddleware that is inspecting incoming request headers and setting a scoped CorrelationId later propagated to all HttpClients.
public class CorrelationId {
public string Value { get;set; }
}
public void ConfigureServices(IServiceCollection services) {
...
services.AddScoped<CorrelationId>();
...
}
I have run into a use case where I need to create an isolated scope around a section of code, but would like the CorrelationId from the scope of the http request to propagate into the isolated scope (The isolated scope has an HttpClient which I would like to have the same header attached).
I would like to spawn off a background Task that is created from DI w/ any required dependencies and for any HttpClients to have headers injected via HttpClientFactory plugins.
public Controller {
public Controller(IServiceProvider serviceProvider, CorrelationId correlationId) { ... }
public IActionResult PostTask() {
var isolatedScope = _serviceProvider.CreateScope();
var action = () => {
using(isolatedScope) {
var backgroundJob = isolatedScope
.ServiceProvider
.GetRequiredService<IBackgroundJob>();
backgroundJob.Execute();
// scopedCorrelationId =/= correlationId
// how can i get correlationId to jump scopes?
}
};
return Task.Factory.StartNew(
action,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
}
Is there a way to transfer certain objects into the isolated scope? Ideally without having to know the typeof(object) i need to transfer.
It is not possible to transfer objects between the parent scope and the isolated scope.
Information like the CorrelationId arriving with the headers of a request better fits with the HttpContext or with an AsyncLocal variable if it needs to be propagated through an async execution flow.

AspNetCore.SignalR SendAsync not firing inside OnConnectedAsync

I am having an issue where I would like to send an event to the frontend whenever somebody is connected to the hub, but the notification is not being received on the front end. I think I may be confused between calling methods directly from the hub vs. utilizing the IHubContext. I was not able to find much information related to these versions, so your help will be greatly appreciated!
Package versions:
Server side (.Net Core 2.2): Microsoft.AspNetCore.SignalR (1.1.0)
Client side (React): #aspnet/signalr:1.1.0
So this is my example Hub:
public class MyHub: Hub<IMyHub>
{
public override async Task OnConnectedAsync()
{
// This newMessage call is what is not being received on the front end
await Clients.All.SendAsync("newMessage", "test");
// This console.WriteLine does print when I bring up the component in the front end.
Console.WriteLine("Test");
await base.OnConnectedAsync();
}
public Task SendNewMessage(string message)
{
return Clients.All.SendAsync("newMessage", message);
}
}
Now the working call I have so far is in a service, but that is sending "newMessage" like so:
public class MessageService: IMessageService
{
private readonly IHubContext<MyHub> _myHubContext;
public MessageService(IHubContext<MyHub> myHubContext)
{
_myHubContext = myHubContext;
}
public async Task SendMessage(string message)
{
// I noticed tis calls SendAsync from the hub context,
// instead of the SendMessage method on the hub, so maybe
// the onConnectedAsync needs to be called from the context somehow also?
await _myHubContext.Clients.All.SendAsync("newMessage", message);
}
}
So the above service method call works and will contact the front end, this is an example of my front end connection in a react component:
const signalR = require('#aspnet/signalr');
class MessageComponent extends React.Component {
connection: any = null;
componentDidMount() {
this.connection = new signalR.HubConnectionBuilder()
.withUrl('http://localhost:9900/myHub')
.build();
this.connection.on('newMessage', (message: string) => {
// This works when called from the service IHubContext
// but not OnConncectedAsync in MyHub
console.log(message);
});
this.connection.start();
}
componentWillUnmount() {
this.connection.stop();
}
render() {
...
}
}
This is because you are using a Strongly Typed Hub (https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-2.2#strongly-typed-hubs).
I assume you defined SendAsync on your IMyHub interface and so the server is sending a message with method = SendAsync, arguments = "newMessage", "test". If you removed your IMyHub type then this will work as expected.

Register RavenDb using Autofac?

Can anyone guide me on how I could register RavenDB using Autofac?
builder.Register<DocumentStore>(.. what after that?
Here is a sample console program that illustrates not only how to wire up the document store, but also how to set it up so you can just inject your document session:
using System.Threading.Tasks;
using Autofac;
using Raven.Client;
using Raven.Client.Document;
namespace ConsoleApplication1
{
internal class Program
{
private static void Main()
{
var builder = new ContainerBuilder();
// Register the document store as single instance,
// initializing it on first use.
builder.Register(x =>
{
var store = new DocumentStore { Url = "http://localhost:8080" };
store.Initialize();
return store;
})
.As<IDocumentStore>()
.SingleInstance();
// Register the session, opening a new session per lifetime scope.
builder.Register(x => x.Resolve<IDocumentStore>().OpenSession())
.As<IDocumentSession>()
.InstancePerLifetimeScope()
.OnRelease(x =>
{
// When the scope is released, save changes
// before disposing the session.
x.SaveChanges();
x.Dispose();
});
// Register other services as you see fit
builder.RegisterType<OrderService>().As<IOrderService>();
var container = builder.Build();
// Simulate some activity. 5 users are placing orders simultaneously.
Parallel.For(0, 5, i =>
{
// Each user gets their own scope. In the real world this would be
// a new inbound call, such as a web request, and you would let an
// autofac plugin create the scope rather than creating it manually.
using (var scope = container.BeginLifetimeScope())
{
// Let's do it. Again, in the real world you would just inject
// your service to something already wired up, like an MVC
// controller. Here, we will resolve the service manually.
var orderService = scope.Resolve<IOrderService>();
orderService.PlaceOrder();
}
});
}
}
// Define the order service
public interface IOrderService
{
void PlaceOrder();
}
public class OrderService : IOrderService
{
private readonly IDocumentSession _session;
// Note how the session is being constructor injected
public OrderService(IDocumentSession session)
{
_session = session;
}
public void PlaceOrder()
{
_session.Store(new Order { Description = "Stuff", Total = 100.00m });
// we don't have to call .SaveChanges() here because we are doing it
// globally for the lifetime scope of the session.
}
}
// Just a sample of something to save into raven.
public class Order
{
public string Id { get; set; }
public string Description { get; set; }
public decimal Total { get; set; }
}
}
Note that DocumentStore is single instance, but DocumentSession is instance per lifetime scope. For this sample, I am manually creating the lifetime scopes and doing it in parallel, simulating how 5 different users might be placing orders at the same time. They will each get their own session.
Putting SaveChanges in the OnRelease event is optional, but will save you from having to put it in every service.
In the real world, this might be a web application, or a service bus application, in which case your session should be scoped to either the single web request or the lifetime of the message, respectively.
If you are using ASP.Net WebApi, you should go get the Autofac.WebApi package off NuGet and use their .InstancePerApiRequest() method, which automatically creates the appropriate lifetime scope.