ABP real-time notification system background jobs - c#

How can I get Online users at server side, to send notifications only to Online users, using a background job?
Background job:
public async Task SendNotifications()
{
await _backgroundJobManager.EnqueueAsync<NotificationJob,
UserIdentifier>(new UserIdentifier(_session.TenantId, _session.UserId.Value), delay: TimeSpan.FromSeconds(3));
}
public override void Execute(UserIdentifier args)
{
var notifications = _userNotificationManager.GetUserNotifications(args);
Abp.Threading.AsyncHelper.RunSync(() => _realTimeNotifier.SendNotificationsAsync(notifications.ToArray()));
}
My Job is on a API where I don't have my session with user info, so I want to get all Online users to publish notifications only to them and after that call my job to notify them.
Does Module Zero by default uses Redis ? As Alper said "No".
I need to connect to SignalR on my API too ? Adding app.MapSignalR(); to my WebApi startupseems to work, but sometimes doesn't, I am missing something
How can I get Online users at web api (server side) ? An alternative I found was to get all subscriptions (GetSubscriptionsAsync) and there I have my userId to send the notifications. var onlineClients = _onlineClients.GetAllClients(); is returning null
Thanks

Related

ASP.NET Core Angular - send different SignalR messages based on logged in user

I have Angular SPA ASP.NET Core app with Identity (IdentityServer4). I use SignalR to push real-time messages to clients.
However I have to "broadcast" messages. All clients receive same messages regardless of what they require and then they figure out in Typescript - do they need this message or not.
What I want is to be able to decide which SignalR client should receive message and what content - it will make messages shorter and cut out processing time on clients completely.
I see there is hub.Client.User(userId) method - thats what I need.. However it appears that the Identity user ID is not known to SignalR.
If I override public override Task OnConnectedAsync() - context inside doesnt have any useful information eg user/principals/claims - are empty.
How can I find out which IdentityServer4 user is connecting to the hub?
EDIT1 suggested implementing IUserIdProvider doesnt work - all xs are null.
https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0#use-claims-to-customize-identity-handling
public string GetUserId(HubConnectionContext connection)
{
var x1 = connection.User?.FindFirst(ClaimTypes.Email)?.Value;
var x2 = connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var x3 = connection.User?.FindFirst(ClaimTypes.Name)?.Value;
...
EDIT2 implemented "Identity Server JWT authentication" from https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0 - doesnt work either - accessToken is empty in PostConfigure
You need to implement IUserIdProvider and register it in the services collection.
Check this question - How to user IUserIdProvider in .NET Core?
There is an obvious solution to it. Here is the sample one can use after creating an asp.net core angular app with identity.
Note that in this particular scenario (Angular with ASP.NET Core with Identity) you do NOT need to implement anything else, in contrary to multiple suggestions from people mis-reading the doc: https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0
Client side:
import { AuthorizeService } from '../../api-authorization/authorize.service';
. . .
constructor(. . . , authsrv: AuthorizeService) {
this.hub = new HubConnectionBuilder()
.withUrl("/newshub", { accessTokenFactory: () => authsrv.getAccessToken().toPromise() })
.build();
Server side:
[Authorize]
public class NewsHub : Hub
{
public static readonly SortedDictionary<string, HubAuthItem> Connected = new SortedDictionary<string, HubAuthItem>();
public override Task OnConnectedAsync()
{
NewsHub.Connected.Add(Context.ConnectionId, new HubAuthItem
{
ConnectionId = Context.ConnectionId,
UserId = Context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value
});
return base.OnConnectedAsync();
}
}
Use it like this:
if(NewsHub.Connected.Count != 0)
foreach (var cnn in NewsHub.Connected.Values.Where(i => !string.IsNullOrEmpty(i.UserId)))
if(CanSendMessage(cnn.UserId)
hub.Clients.Users(cnn.UserId).SendAsync("servermessage", "message text");
It is transpired that User data is empty in SignalR server context because authorization doesnt work as I expected it to. To implement SignalR authorization with Identity Server seems to be a big deal and is a security risk as it will impact the whole app - you essentially need to manually override huge amount of code which already is done by Identity Server just to satisfy SignalR case.
So I came up with a workaround, see my answer to myself here:
SignalR authorization not working out of the box in asp.net core angular SPA with Identity Server
EDIT: I missed an obvious solution - see the other answer. This is still valid workaround though, so I am going to let it hang here.

Check in Azure Functions if client is still connected to SignalR Service

I've created negotiate and send message functions in Azure Functions (similar to the samples below) to incorporate the SignalR Service. I'm setting UserId on the SignalRMessage by using a custom authentication mechanism.
https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-signalr-service?tabs=csharp
[FunctionName("negotiate")]
public static SignalRConnectionInfo Negotiate(
[HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req,
[SignalRConnectionInfo
(HubName = "chat", UserId = "{headers.x-ms-client-principal-id}")]
SignalRConnectionInfo connectionInfo)
{
// connectionInfo contains an access key token with a name identifier claim set to the authenticated user
return connectionInfo;
}
[FunctionName("SendMessage")]
public static Task SendMessage(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]object message,
[SignalR(HubName = "chat")]IAsyncCollector<SignalRMessage> signalRMessages)
{
return signalRMessages.AddAsync(
new SignalRMessage
{
// the message will only be sent to this user ID
UserId = "userId1",
Target = "newMessage",
Arguments = new [] { message }
});
}
I'd like to send a push notification if the client is no longer connected instead of adding a new object to the IAsyncCollector. I've also set up AppCenter push framework properly, but I'm facing an issue. Is there an easy way to find out which UserId is still connected to the hub? This way, I could decide to send a push. What is the recommended Microsoft guidance on this issue?
Have a look at this feature: Azure SignalR Service introduces Event Grid integration feature, where the SignalR Service emits two following event types:
Microsoft.SignalRService.ClientConnectionConnected
Microsoft.SignalRService.ClientConnectionDisconnected
More details here.

Azure application insights drops some custom events

I have a Web Application and want to track user logins in AppInsights with the following code:
[HttpPost]
public async Task<ActionResult> Login(AccountViewModel model, string returnUrl)
{
var success = await _userManager.CheckPasswordAsync(model);
_telemetryClient.TrackEvent("Login",
new Dictionary<string, string>
{
{ "UserName", model.UserName },
{ "UsingPin", model.UsingPin.ToString()},
{ "ReturnUrl", returnUrl},
{ "LastUsedUrl", model.LastUsedUrl },
{ "Success", success.ToString() }
});
return View(model);
}
The code works, I get events in analytics portal... but only a subset of events! In the sessions table on the database side I have about 1000 unique user logins, but AI reports around 100...
What I have already checked:
_telemetryClient is a singleton configured through Unity (<lifetime type="singleton" />)
there is no quota configured in Azure portal
I have more than 1000 POST requests to the Login method, this looks just right (I mean every request comes to this exact server, returns 200 status, etc.)
So it looks like Azure just drops some of custom events... or they are not send to azure for some reason. Is there any configuration part I am missing?
As far as I know, if your application sends a lot of data and you are using the Application Insights SDK for ASP.NET version 2.0.0-beta3 or later, the adaptive sampling feature may operate and send only a percentage of your telemetry.
So I guess this the reason why you just see 100 records in the AI.
If you want to send all the records to the AI, I suggest you could disable the adaptive sampling. More details about how to disable the adaptive sampling, you could refer to this article.
Notice: This is not recommended. Sampling is designed so that related telemetry is correctly transmitted, for diagnostic purposes.

How to create custom authentication mechanism based on HTTP header?

I'm leaving old version of question on a bottom.
I'd like to implement custom authentication for SignalR clients. In my case this is java clients (Android). Not web browsers. There is no Forms authentication, there is no Windows authentication. Those are plain vanilla http clients using java library.
So, let's say client when connects to HUB passes custom header. I need to somehow authenticate user based on this header. Documentation here mentions that it is possible but doesn't give any details on how to implement it.
Here is my code from Android side:
hubConnection = new HubConnection("http://192.168.1.116/dbg", "", true, new NullLogger());
hubConnection.getHeaders().put("SRUserId", userId);
hubConnection.getHeaders().put("Authorization", userId);
final HubProxy hubProxy = hubConnection.createHubProxy("SignalRHub");
hubProxy.subscribe(this);
// Work with long polling connections only. Don't deal with server sockets and we
// don't have WebSockets installed
SignalRFuture<Void> awaitConnection = hubConnection.start(new LongPollingTransport(new NullLogger()));
try
{
awaitConnection.get();
Log.d(LOG_TAG, "------ CONNECTED to SignalR -- " + hubConnection.getConnectionId());
}
catch (Exception e)
{
LogData.e(LOG_TAG, e, LogData.Priority.High);
}
P.S. Original question below was my desire to "simplify" matter. Because I get access to headers in OnConnected callback. I thought there is easy way to drop connection right there..
Using Signal R with custom authentication mechanism. I simply check if connecting client has certain header passed in with connection request.
Question is - how do I DECLINE or NOT connect users who don't pass my check? Documentation here doesn't really explain such scenario. There is mentioning of using certificates/headers - but no samples on how to process it on server. I don't use Forms or windows authentication. My users - android java devices.
Here is code from my Hub where I want to reject connection..
public class SignalRHub : Hub
{
private const string UserIdHeader = "SRUserId";
private readonly static SignalRInMemoryUserMapping Connections = new SignalRInMemoryUserMapping();
public override Task OnConnected()
{
if (string.IsNullOrEmpty(Context.Headers[UserIdHeader]))
{
// TODO: Somehow make sure SignalR DOES NOT connect this user!
return Task.FromResult(0);
}
Connections.Add(Context.Headers[UserIdHeader], Context.ConnectionId);
Debug.WriteLine("Client {0}-{1} - {2}", Context.Headers[UserIdHeader], Context.ConnectionId, "CONNECTED");
return base.OnConnected();
}
So I just created a custom Authorization Attribute and overrode the AuthorizeHubConnection method to get access to the request and implemented the logic that you were trying to do with the Header and it appears to be working.
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
namespace SignalR.Web.Authorization
{
public class HeadersAuthAttribute : AuthorizeAttribute
{
private const string UserIdHeader = "SRUserId";
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
{
if (string.IsNullOrEmpty(request.Headers[UserIdHeader]))
{
return false;
}
return true;
}
}
}
Hub
[HeadersAuth]
[HubName("messagingHub")]
public class MessagingHub : Hub
{
}
Which yields this in the console (if the picture doesn't show up, it's a [Failed to load resource: the server responded with a status of 401 (Unauthorized)]):
In fact, accepted answer is wrong. Authorization attribute, surprisingly, shall be used for authorization (that is, you should use it for checking whether requesting authenticated user is authorized to perform a desired action).
Also, since you using incorrect mechanics, you don't have HttpContext.Current.User.Identity set. So, you have no clear way to pass user info to your business / authorization logic.
And third, doing that you won't be able to use Clients.User() method to send message to specific user, since SignalR will be not able to map between users and connections.
The correct way is to plug in into OWIN authentication pipeline. Here is an excellent article explaining and demonstrating in detail how to implement custom authentication to be used in OWIN.
I not going to copy-paste it here, just follow it and make sure you implement all required parts:
Options
Handler
Middleware
After you have these, register them into OWIN:
app.Map("/signalr", map =>
{
map.UseYourCustomAuthentication();
var hubConfiguration = new HubConfiguration
{
Resolver = GlobalHost.DependencyResolver,
};
map.RunSignalR(hubConfiguration);
});

SignalR - Send message to user using UserID Provider

Using SignalR, I believe I should be able to send messages to specific connected users by using UserID Provider
Does anyone have an example of how this would be implemented? I've searched and searched and can not find any examples. I would need to target a javascript client.
The use case is, users to my site will have an account. They may be logged in from multiple devices / browsers. When some event happens, I will want to send them a message.
I have not looked into SignalR 2.0 but I think this is an extension of what the previous versions of SignalR used to have. When you connect to the hub you can decorate it with an Authorize attribute
[HubName("myhub")]
[Authorize]
public class MyHub1 : Hub
{
public override System.Threading.Tasks.Task OnConnected()
{
var identity = Thread.CurrentPrincipal.Identity;
var request = Context.Request;
Clients.Client(Context.ConnectionId).sayhello("Hello " + identity.Name);
return base.OnConnected();
}
}
As you can see you are able to access the Identity of the user accessing the Hub. I believe the new capability would be nothing more than an extension of this. Since the connection is always kept alive between the client and the hub you will always have the principal identity which will give you the UserId.
I believe this can help you: (linked from here)
A specific user, identified by userId.
Clients.User(userid).addContosoChatMessageToPage(name, message);
The userId can be determined using the IUserId interface:
public interface IUserIdProvider
{
string GetUserId(IRequest request);
}
The default implementation of IUserIdProvider is PrincipalUserIdProvider. To use this default implementation, first register it in GlobalHost when the application starts up:
var idProvider = new PrincipalUserIdProvider();
GlobalHost.DependencyResolver.Register (typeof(IUserIdProvider), () => idProvider);
The user name can then be determined by passing in the Request object from the client.

Categories