Send messages to SPECIFIC user

I have implemented push messages to channels which works
fine. Now I try to send messages to SPECIFIC users if they are connected
or as soon as they connect.

I have a few troubles with that:

  1. What do I need to register on the client side? For channels I did
    PushMessageClient = new
    ServerEventsClient($“http://{serverName}:{port}”, channels:
    “LsInfoChannel”);
    and then I registered a named receiver.
  2. In the docs I saw there are three methods to send to a specific user (NotifyUserId, NotifyUserName, NotifySession). But what do I need to register on the client instead of a named receiver with channels?
  3. On the server I also have a little problem: The service sends a Message to RabbitMQ and it embeds the username to the message. Some time later there will be a processing confirmation or an error coming back. I like to send this message to the UI using server side events. But in the service the UserName and UserAuthId properties are NULL. The username seems to be stored in the UserAuthName property of my session object. So how can I send a message to a specific user?
  4. Is it correct that I need Redis to make those messages persistent? I currently use the memory based events but they wont survive a reboot of the server…

I don’t understand where the issue is, to send a notification to a different User you can just call NotifyUserId. The client doesn’t have to do anything different, Registration handlers remains the same except that notification will only be sent to the User with that Id. Review the Chat implementation to see a working example of this.

If they’re null, they’re either not being initialized or serialized property. Resolve that issue first then send notifications using UserId or Username using the NotifyUserId or NotifyUsername APIs.

There is no persistence of Server Events Notificatoins which are only sent to active connections, if a User isn’t connected they will not receive the notification. Your App will need to maintain its own persistence if you need to, which the Chat implementation above also shows an example of with its ChatHistory dependency.

Hi Demis,
Thanks for the help. I have looked at the Chat client, even though I am not a JavaScript guy I guess I understand what is going on.

My problem seems to be my own implementation of the CredentialsAuthProvider. I don’t know if something has changed with Version 5 (apart from the password hashing). I was looking at the Source and I saw that some of my missing information is filled in the AuthenticateResponse object in one of the Authenticate() methods. In my implementation I do: (sorry somehow this editor does not recognise my indent (tabs or spaces…)

public class LsAuthProvider : CredentialsAuthProvider
{
    protected new static ILog Log = LogManager.GetLogger(LsConstants.LoggerName);

    public override object Logout(IServiceBase service, Authenticate request)
    {
        // some code....
        return base.Logout(service, request);
    }

    public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
    {
        //let normal authentication happen
        Log.Debug($"Starting custom authentication method 'BizBusAuthProvider'... {session.UserAuthId}");
        var authResponse = (AuthenticateResponse) base.Authenticate(authService, session, request);
        var user = (LsUser) authService.Request.Items["AppUser"];

        authResponse.Meta = new Dictionary<string, string>();
        authResponse.Meta.Add("ResetPwdRequired", user.MustChangePwd.ToString());
        authResponse.Meta.Add("IsLockedOut", user.IsLockedOut.ToString());
        authResponse.Meta.Add("Email", user.Email);
        authResponse.Meta.Add("FullName", user.UserName);
        authResponse.Meta.Add("RemoteIP", authService.Request.RemoteIp);

        return authResponse;
    }

    public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
    {
         //some code...
    }
}

In your code I found:

return new AuthenticateResponse
{
UserId = session.UserAuthId,
UserName = userName,
SessionId = session.Id,
DisplayName = session.DisplayName
?? session.UserName
?? $"{session.FirstName} {session.LastName}".Trim(),
ReferrerUrl = referrerUrl
};

My session object has almost no properties filled( see screenshot) from a breakpoint. Looks like I miss something in my Authenticate(...) method or in my IUserAuth implementation. I am not using ServiceStacks Self Registration feature. My Repository for user accounts is a Redis Database. The question is, where Do I have to fill those missing properties and which ones are required internally. At least the ones I marked yello I need for server side events…

Thanks for any hints that could help me solve this issue.

The UserName is sourced from Authenticate.UserName, is that populated on the Authenticate Request DTO?

Otherwise the session is saved at the end where it should save everything it’s populated with.

Are you using the RedisAuthRepository or your own custom IAuthRepository?

Can you also provide the raw HTTP Request/Response for your authentication request (with all sensitive info scrubbed out with xxxx).

Ok, I think I got it solved! I saw that there is a method OnAuthenticated in CredentialsAuthPrivider where I can set the required props to the session object. So the required data to use NotifyUserId() and NotifyUserName() is there.

But still something seems to be missing. I wrote a primitive service method to see a bit more about my registered subscribers like so:

    [RequiresAnyRole("Administrator")]
    public void Get(GetPushSubscribtionInfos request)
    {
        Log.Debug($$"GetPushSubscribtionInfos() sessionProps: UserAuthID: '{GetSession().UserAuthId}' | UserAuthName: '{GetSession().UserAuthName}' | " +
                  $$"UserName: {GetSession().UserName} | SessionId: {GetSession().Id} | AuthProvider: {GetSession().AuthProvider} | IsAuthenticated: {GetSession().IsAuthenticated}");

        try
        {
            var subInfos = ServerEvents.GetAllSubscriptionInfos();
            Log.Debug($$"We have currently {subInfos.Count} subscriptions registered.");
            foreach (var si in subInfos)
            {
                Log.Debug($$"Subscription Info: Created: {si.CreatedAt} | IsAuthenticated: {si.IsAuthenticated} | " +
                          $$"UserId: {si.UserId} | UserName: {si.UserName} | SessionID: {si.SessionId} | SubscriptionID: {si.SubscriptionId} | " +
                          $$"UserAddress: {si.UserAddress}");
            }
        
            //return subInfos;
        }
        catch (Exception ex)
        {
            Log.Error($"Error retrieving push message subscription infos. Error: {ex}");
            throw;
        }

    }

For testing I currently call this from the client, whenever it receives a heartbeat. So I expected to see one authenticated session to the (currently only client which is my WPF app). So the subscription is visible but the session is unauthenticated!

The output in my log looks like so:

2018-02-10 19:34:48,504; [14]; DEBUG; BizBusLicenseServer; [ServerManagementService.Get]; - GetPushSubscribtionInfos() sessionProps: UserAuthID: '1' | UserAuthName: 'BizBusLicenseAdmin' | UserName: BizBusLicenseAdmin | SessionId: gxqFRcfEqlilxyOZROBz | AuthProvider: credentials | IsAuthenticated: True
2018-02-10 19:34:48,513; [14]; DEBUG; BizBusLicenseServer; [ServerManagementService.Get]; - We have currently 1 subscriptions registered.
2018-02-10 19:34:48,521; [14]; DEBUG; BizBusLicenseServer; [ServerManagementService.Get]; - Subscription Info: Created: 10.02.2018 18:34:08 | IsAuthenticated: False | UserId: -1 | UserName:  | SessionID: sUxYRrY6nPa8cBmQjs7P | SubscriptionID: v5158maEKkjkzsGZ2HcJ | UserAddress: fe80::d981:983f:79f2:ce44%3

To me this looks strange: We have two different sessions (at least there are two different IDs), one is authenticated, the other is not. And of course if the session is NOT authenticated there is no UserName and no chance to send a message to a user.

So what do I make wrong here?

And one more thing: Do you know, why I see sometimes an IPv4 address and sometimes an IPv6 address? I wonder what address this is, I have disabled IPv6 on this box here!!

I wont be able to tell anything from here but the Session is determined by the initial request to the /event-stream where the Session Cookies will need to refer to an Authenticated UserSession which is saved by the AuthProvider.

The Unauthenticated Session may be the connection before the User was authenticated and the Authenticated Subscription a new SSE subscription after. You can register a ServerEventsFeature.OnCreated delegate to inspect all the SSE subscriptions that are created. If the SSE connection was for the previously authenticated user that’s no longer connected it will eventually be dropped by the server when the Heartbeat IdleTimeout elapses.

The user/session info on the SSE subscription then lasts for the lifetime of the connection, the only thing that changes it is if the connection is dropped and auto reconnects at which point a new SSE subscription is created with the current Session info.

This is just what’s returned by the underlying Web Server Request context, e.g. for .NET Core it’s returning HttpContext.Connection.RemoteIpAddress:

Hi Demis,
First many thanks for your great and lightning fast help, really awesome!!

Yeah I see that timing is very important and also the values for session timeouts, heartbeats and IdleTimeouts need to be ‘coordinated’. I saw that I started my ServerEventsClient before the authentication (Login) was completed! So I moved that but still no authenticated sessions! The .OnConnect and .OnCreated ServerEventFeatures are a big help and show some really strange output!

2018-02-11 09:52:48,837; [14]; INFO ; BizBusLicenseServer; [AppHost+<>c.<Configure>b__15_7]; - ONCREATED EVENT: SessionProps: UserAuthID: '' | UserAuthName: '' | UserName: '' | SessionId: '6pZVPlkLIuZfbAOiQkij' | AuthProvider: '' | IsAuthenticated: 'False' | Request: 'ServiceStack.Host.HttpListener.ListenerRequest' | Subscription: ''
2018-02-11 09:52:48,848; [14]; INFO ; BizBusLicenseServer; [AppHost+<>c.<Configure>b__15_6]; - ONCONNECT EVENT: Subscription:  | Authenticated: False

There is a session id but no other values! Sometimes I see a username which is set to ‘User1’! No idea where this comes from! So I have to dig a bit deeper here.

Just to make sure I understand things right: The following client registrations are correct?

    public void InitServiceClients(string serverName)
    {
        LicenseServiceClient = new JsonServiceClient($$"http://{serverName}:6090");
        //TODO: improve this, naming ??
        LicensePushMessageClient = new ServerEventsClient($$"http://{serverName}:6090", channels: "LsInfoChannel")
        {
            OnConnect = PrintConnectMessage,
            OnMessage = PrintMessage,
            OnHeartbeat = PrintHeartbeat,
        };
    }

JsonServiceClient and ServerEventsClient on the same port ?

And my current client is a WPF application. Windows Fat clients do not have cookies as Web Browsers, at least they are not stored somewhere, sessions are in Memory, in fact I use Redis as caching provider (definable in the config file of the server):

        var cachingProvider = Settings.Default.CacheProvider;
        if (cachingProvider.CompareIgnoreCase("Memory") == 0)
        {
            container.Register<ICacheClient>(new MemoryCacheClient());
        }
        else if (cachingProvider.CompareIgnoreCase("Redis") == 0)
        {
            container.Register(c => RedisCachePool.GetCacheClient());
        }

This should work also for non-web applications right?

I will debug further and come back, if I have more details.

Sometimes I see a username which is set to ‘User1’! No idea where this comes from!

User{N} is the Username given to anonymous users.

Yes both JsonServiceClient and ServerEventsClient should use the same BaseUrl where ServiceStack is hosted.

Windows Fat clients do not have cookies

Most HTTP Clients have cookies, all .NET HTTP Clients included.

The docs show an example of Authenticating to ServerEvents with C# Client, i.e. Authenticate with the ServerEventClient ServiceClient

client.ServiceClient.Post(new Authenticate {
    provider = "credentials",
    UserName = "user",
    Password = "pass",
    RememberMe = true,
});

Which will populate the Session Cookies on the HTTP Client to establish an Authenticated UserSession, then you can Start the ServerEventsClient connection to connect as an Authenticated User:

client.Start();

Hi Demis,

Yeah, Authenticate in the ServiceClient did the trick! I read this but did not really understand it since the messages all came through but they where anonymous. Now I added this inside my login ViewModel (controller) where I have username and password and then Start() the client and things work as expected.

Now I have to play around with use cases such as interrupted connections etc. My session timeout is a lot longer than the IdleTimeout for ServerEvents (users don’t want to login again if they where 10 minutes inactive…).

Also I have to kind of guarantee the delivery of certain messages which requires persistence…

But you helped me to get one step further. Thanks again!

1 Like