Although this post is written with a focus on using F# and Fable.SignalR, the concepts talked about are applicable to SignalR in general, including when used with C#
When using SignalR, one of the most important things will be to manage the various connections made by the clients, as well as being able to send messages to specific clients. SignalR offers three different ways of specifying connections and clients, each with their distinct usages. The question comes down when to use what, and the advantages and disadvantages offered by each.
We're going to continue to build off the sample chat application from the previous SignalR blog posts. We're going to implement authentication using Auth0, but any identity provider (e.g. Keycloak or IdentityServer) should suffice. Unfortunatley, we're relying on specific Xamarin.Essentials features that aren't available in WPF, so that our new features will only work on Android. But that doesn't matter, since we can use this opportunity to "incentivize" users to switch to the android app ;)
This is Part Three of my F# SignalR tutorial series.
- Part One: https://hashset.dev/article/13_real_time_communication_with_giraffe_and_fabulous
- Part Two: https://hashset.dev/article/14_improving_real_time_communication_using_fable_signal_r
Source code for sample app can be found here, in the
- GitHub: https://github.com/kaeedo/FabulousSignalRTutorial/tree/UserManagement
- CodeBerg: https://codeberg.org/CubeOfShame/FabulousSignalRTutorial/src/branch/UserManagement
SignalR provides three different ways to manage connections. You can specify a
ConnectionID, add clients to a
Group, or by referencing specific authenticated
Users. They all have their own use cases, and in a large application would most likely all be used at different times. But for our simple chat application, we can choose which one to use, and explore how it works.
A Connection ID refers to a specific client connection to the server. Client in this case refers to the specific SignalR client deployed on a site/app, and connection refers to the active connection established by said client. In practice, this means that even if a given User is logged in e.g. on their phone and on a Web App, those would represent two different connection IDs. Additionally, if the client drops the connection for a moment and the automatic reconnect kicks in, the client would get a new connection ID.
In the context of our chat application, relying on the connection ID is insufficient. Even before we have a concept of a single User using multiple clients at the same time, we would still need to maintain a mapping of a client with whatever connection ID is currently active for that client. This can quickly get out of hand. That's not to say that using connection IDs is a bad idea, but just the first step in using the right tool for the right job.
A SignalR group is an arbitrary collection of connections assigned to a named group. A group can be created at any point, or a connection can be added to an existing group at any time. When sending a message to a group, it will be sent to all connections associated with that group. Additionally, whenever a connection disconnects, it will automatically be removed from any associated groups as well. However, this also means that when a client reconnects, it will need to be re-added to any groups that it should be a member of. A single connection can also be a member of multiple groups.
For our chat application, groups are a very good solution for our clients. Since a user joins the chat room by entering a username, we can create a SignalR group using the username as the group name, and add the connection ID to this group. Additionally, we can do the same on reconnect. This already allows the same user to enter the chat room on multiple end user applications if we had them. However, in this specific scenario we have to be careful as there is no validation of any kind. We still need to persist the existing groups in the backend somehow, and in our example server application, we are doing this with an In Memory dictionary. Then we can send a message to all groups, or specific groups by checking to see what exists within our store.
As SignalR sits on top of ASP.NET Core, we can leverage functionality that exists in there. One of these features is ASP.NET Core Authentication and User Management. Once a User is authenticated with the web server, SignalR itself also knows about the same authentication mechanism. This means that within a SignalR Hub we can access the
HTTP Context User object, and all related properties. This allows us to check all manner of information that exists on the authenticated user, as well as any authorization policies. We can then check the
UserId property to find specific users no matter how they are connected or how many connections they are associated with.
Within the Chat Application, we must now add a mechanism to allow a user to log in and authenticate against our server. I have decided to use Auth0 for the example app, because it's relatively simple, and also a hosted solution. But any Identity Provider solution that works with ASP.NET Core authentication should work. In previous iterations of the sample app, users could send a direct message to another specific user. We are now going to move this functionality so that only authenticated users may use this feature. On the backend, it is trivial to implement as we can query the
IsAuthenticated property on the
User object to see if a client is allowed to send a direct message.
In this section I will describe how to add authentication via Auth0 to your Giraffe backend, in such a way that Fable.SignalR can also understand it. Once again, the concepts talked about here also apply to general ASP.NET Core, including when used with C#, however, as we're building off the same sample chat application as previous SignalR posts, I'm going to focus on F# with Giraffe and Fable.SignalR on the backend, and F# in Xamarin using Fabulous.
Adding login functionality to the client is straightforward. Since we have the Xamarin.Essentials package, we can utilize the Web Authenticator. The gist of how it works, is that any configuration to the identity provider is handled on a server backend, and utilizes whatever WebView is installed on the client to facilitate an authorization grant. The advantage of this is that configuration details such as client ID and client secret are all exclusively on the backend, as well as allowing and updates to the authentication happen entirely away from the client, so that all clients would receive the updates immediately.
On top of that, there is a technical reason in the Chat Application, because Auth0's Xamarin client libraries are meant to be included directly in the Android/iOS project instead of a shared codebase. Usually, this wouldn't be a problem, but since we like to stay within F# land and Fabulous, we are doing everything in the shared library, so we have to rely on the Web Authenticator to facilitate the login flow.
To actually start the login flow, we dispatch a message and receive an idToken from the backend, which we can save in our model.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
That's how simple it is to implement login on the client using the Web Authenticator. The idToken we receive from the backend can then be parsed as any
JWT token and we can extract any information we want. e.g.:
1: 2: 3: 4: 5:
Additionally, we need to send the idToken to the SignalR hub when connecting. Expand the
RegisterHub configuration like this:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
We can pass the idToken into the
AccessTokenProvider. Keep in mind, this function gets called for every HTTP request made by SignalR.
Lastly, we must also configure the actual entrypoint projects to handle the callback URI. Check the docs here to read up more about it.
I won't describe a step by step guide here on how to configure Auth0, but only touch upon the most important parts. Make sure to read up on the various capabilities of Auth0 to ensure that you configure it for your usecase. That being said, I will touch upon the important points within the Auth0 config that I made to get it working for my application. Additionally, for my sample application, I didn't implement any kind of User Sign Up. I created the users directly in Auth0 just so I could directly start working on the authentication part.
In Auth0, create a new Application of type
Regular Web Application. In the settings section, we need to remember the
Client ID, and
Client Secret. Make sure not to share the secret with anyone. I personally use the .NET tool user-secrets to manage my secrets for my hobby projects, but you can use whatever you want. Ensure that the
Token Endpoint Authentication Method is set to
Basic. Lastly, configure a callback URL, something like this:
https://localhost:5001/callback, https://127.0.0.1:5001/callback even though we won't be using it in our server application. After that, as mentioned previously, go to the User Management section of Auth0 to create a few test users.
And that was already all the Auth0 configuration needed. Now for the server config.
Since Giraffe is built on top of ASP.NET Core, we only need to include the existing Authentication middleware in our pipeline, and configure it to utilize our Auth0 identity provider. To start, add the following NuGet packages
Microsoft.AspNetCore.Authentication.OpenIdConnect. After that, we can add in the authentication middleware within the
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48:
We must also create an
/authenticate route that the WebAuthenticator from Xamarin.Essentials will call. This is then where the redirect to Auth0 happens, and access tokens are handed back. All on the server side:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
And lastly, we need to tell our SignalR Hub to make use of the authentication. I've opted to refactor the SignalR configuration using the builder pattern, which looks like this:
1: 2: 3: 4: 5: 6: 7:
That should be it for changes on the server, after which we can make use of the authenticated users within our
send handlers by checking the
HubContext.Context.User object. It has useful properties such as
IsAuthenticated, but also the entire User object with any configured claims as defined by OpenIDConnect. It also importantly contains a UserId that we can utilize to identify a unique user no matter what client they're connecting from, and can send a Fable SignalR message directly to them like this:
Connecting Auth0 to the ASP.Net Core authentication pipeline expects to use the Cookie Authentication Scheme, but Fable.SignalR expects the default authentication scheme to be JWT Bearer Authentication. These will clash with each other as you can't specify for which routes which scheme should be considered the default. What can be done though, is how I have configured it within the
AddAuthentication options. There is is possible to define the default authentication scheme for specific actions. We can define the default scheme to be JWT Bearer Authentication, but then redefine only the Sign In scheme to utilize Cookie Authentication.