In Part 1 I introduced a basic usage of SignalR and talked about the goals we were trying to accomplish with the library.
In the next few posts I’m going to show how we can build a real-time user notification and session management system for a web application.
In this post I’ll show how we can implement a solution that accomplishes our goals.
Before diving back into SignalR it’s important to have a quick rundown of concepts for session management. If we think about how sessions work for a user in most applications it’s usually conceptually simple. A session is a mechanism to track user rights between the user logging in and logging out. A session is usually tracked through a cookie attached to each request made to the server. A user has a session (or multiple sessions if they are logged in from another machine/browser) and each session is tied to a request or connection. Each time the user requests a page a new connection is opened to the server. As long as the session is active each connection is authorized to do whatever it needs to do (as defined by whatever authorization policies are in place).
When you kill a session each subsequent connection for that session is denied. The session is dead, no more access. Simple. A session is usually killed when a user explicitly logs out and destroys the session cookie or the browser is closed. This doesn’t normally kill any other sessions tied to the user though. The connections made from another browser are still authorized.
From a security perspective we may want to notify the user that another session is already active or was just created. We can then allow the user to destroy the other session if they want.
SignalR works really well in this scenario because it solves a nasty problem of timing. Normally when the server wants to tell the client something it has to wait for the client to make a request to the server and then the client has to act on the server’s message. A request to the server is usually only done when a user explicitly clicks something, or there’s a timer polling every 30 seconds or so. If we want to notify the user instantly of another session we can’t necessarily wait for the client to call. SignalR solves this problem because it can call the client directly from the server.
Now, allowing a user to control other sessions requires tracking sessions and connections. If we follow the diagram above we have a pretty simple relationship between users and sessions, and between sessions and connections. We could store this information in a database or other persistent storage, and in fact would want to for non-trivial applications, but for the sake of this post we’ll just store the data in memory.
Most session handlers these days (e.g. the SessionAuthenticationModule in WIF) create a cookie that contains everything the web application should know about the user. As long as that identity in the cookie is valid the user can do whatever the session handler allows. This is a mostly stateless process and aligns with various tenants of REST. Each request to the server contains the identity of the user, and the server doesn’t have to track anything. It’s simple and powerful.
However, in non-trivial applications this doesn’t always cut it. Security sometimes requires state. In this case we require state in the sense that the server needs to track all active sessions tied to a user. For this we’ll use the WIF SessionAuthenticationModule (SAM) and a custom SessionSecurityTokenHandler.
Before we can validate a session though, we need to track when a session is created. If the application is configured for federation you can create a customClaimsAuthenticationManager and call the session creation code, or if you are creating a session token manually you can call this code on login.
void CreateSession() { string sess = CreateSessionKey();
var principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, “myusername”), new Claim(ClaimTypes.Sid, sess) }, AuthenticationTypes.Password) });
var token = FederatedAuthentication.SessionAuthenticationModule.CreateSessionSecurityToken(principal, “mycontext”, DateTime.UtcNow, DateTime.UtcNow.AddDays(1), false);
FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(token);
NotificationHub.RegisterSession(sess, principal.Identity.Name); }
private string CreateSessionKey() { var rng = System.Security.Cryptography.RNGCryptoServiceProvider.Create();
var bytes = new byte[32];
rng.GetNonZeroBytes(bytes);
return Convert.ToBase64String(bytes); }
We’ll get back to the NotificationHub.RegisterSession method in a bit.
After the session is created, on subsequent requests the SessionSecurityTokenHandlervalidates whether a user’s session is still valid and authorized. The SAM calls the token handler when it receives a session cookie and generates an identity for the current request.
From here we can determine whether the user’s session was forced to logout. If we override the ValidateSession method we can check against the NotificationHub. Keep in mind this is an example – it’s not a good design decision to track session data in your notification hub. I’m also using ClaimTypes.Sid, which isn’t the best claim type to use either.
protected override void ValidateSession(SessionSecurityToken securityToken) { base.ValidateSession(securityToken);
var ident = securityToken.ClaimsPrincipal.Identity as IClaimsIdentity;
if (ident == null) throw new SecurityTokenException();
var sessionClaim = ident.Claims.Where(c => c.ClaimType == ClaimTypes.Sid).FirstOrDefault();
if(sessionClaim == null) throw new SecurityTokenExpiredException();
if (!NotificationHub.IsSessionValid(sessionClaim.Value)) { throw new SecurityTokenExpiredException(); } }
Every time a client makes a request to the server the user’s session is validated against the internal list of valid sessions. If the session is unknown or invalid an exception is thrown which kills the request.
To configure the use of this SecurityTokenHandler you can add it to the web.config in themicrosoft.identityModel/service section. Yes, this is still WIF 3.5/.NET 4.0. There is no requirement for .NET 4.5 here.
<securityTokenHandlers> <remove type=”Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler, Microsoft.IdentityModel” /> <add type=”Syfuhs.Demo.CustomSessionSecurityTokenHandler, MyDemo” /> </securityTokenHandlers>
Now that we can track sessions on the server side we need to track connections. To start tracking connections we need to start at our Hub. If we go back to our NotificationHub we can override a few methods, specifically OnConnected and OnDisconnected. Every time a page has loaded the SignalR hubs client library, OnConnected is called and every time the page is unloaded OnDisconnected is called. Between these two methods we can tie all active connections to a session. Before we do that though we need to make sure that all requests to our Hub are only from logged in users.
To ensure only active sessions talk to our hub we need to decorate our hub with the[Authorize] attribute.
[Authorize(RequireOutgoing = true)] public class NotificationHub : Hub { // snip }
Then we override the OnConnected method. Within this method we can access what’s called the ConnectionId, and associate it to our session. The ConnectionId is unique for each page loaded and connected to the server.
For this demo we’ll store the tracking information in a couple dictionaries.
private static readonly Dictionary<string, string> UserSessions = new Dictionary<string, string>();
private static readonly Dictionary<string, List<string>> sessionConnections = new Dictionary<string, List<string>>();
public override Task OnConnected() { var user = Context.User.Identity as IClaimsIdentity;
if (user == null) throw new SecurityException();
var sessionClaim = user.Claims.Where(c => c.ClaimType == ClaimTypes.Sid).FirstOrDefault();
if (sessionClaim == null) throw new SecurityException();
sessionConnections[sessionClaim.Value].Add(Context.ConnectionId);
return base.OnConnected(); }
On disconnect we want to remove the connection associated with the session.
public override Task OnDisconnected() { var user = Context.User.Identity as IClaimsIdentity;
if (user == null) throw new SecurityException();
var sessionClaim = user.Claims.Where(c => c.ClaimType == ClaimTypes.Sid).FirstOrDefault();
if (sessionClaim == null) throw new SecurityException();
sessionConnections[sessionClaim.Value].Remove(Context.ConnectionId);
return base.OnDisconnected(); }
Now at this point we can map all active connections to their various sessions. When we create a new session from a user logging in we want to notify all active connections that the new session was created. This notification will allow us to kill the new session if necessary. Here’s where we implement that NotificationHub.RegisterSession method.
internal static void RegisterSession(string sessionId, string user) { UserSessions[sessionId] = user; sessionConnections[sessionId] = new List<string>();
var message = “You logged in to another session”;
var context = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();
var userCurrentSessions = UserSessions.Where(u => u.Value == user);
foreach (var s in userCurrentSessions) { var connectionsTiedToSession = sessionConnections.Where(c => c.Key == s.Key).SelectMany(c => c.Value);
foreach (var connectionId in connectionsTiedToSession) context.Clients.Client(connectionId).sessionRegistered(message, sessionId); } }
This method will create a new session entry for us and look up all other sessions for the user. It will then loop through all connections for the sessions and notify the user that a new session was created.
So far so good, right? This takes care of almost all of the server side code. But next we’ll jump to the client side JavaScript and implement that notification.
When the server calls the client to notify the user about a new session we want to write the message out to screen and give the user the option of killing the session.
HTML:
<div class=”notification”></div>
JavaScript:
var notifier = $.connection.notificationHub;
notifier.client.sessionRegistered = function (message, session) { $(‘.notification’).text(message);
$(‘.notification’).append(‘<a class=”killSession” href=”#”>End Session</a>’); $(‘.notification’).append(‘<a class=”DismissNotification” href=”#”>Dismiss</a>’); $(‘.killSession’).click(function () { notifier.server.killSession(session); $(‘.notification’).hide(500); });
$(‘.DismissNotification’).click(function () { $(‘.notification’).hide(500); }); };
On session registration the notification div text is set to the message and a link is created to allow the user to kill the session. The click event calls the NotificationHub.KillSessionmethod.
Back in the hub we implement the KillSession method to remove the session from the list of active sessions.
public void KillSession(string session) { var connections = sessionConnections[session].ToList();
sessionConnections.Remove(session); UserSessions.Remove(session);
foreach (var c in connections) { Clients.Client(c).sessionEnded(); } }
Once the session is dead a call is made back to the clients associated with that session to notify the page that the session has ended. Back in the JavaScript we can hook into the sessionEnded function and reload the page.
notifier.client.sessionEnded = function () { location.reload(); }
Reloading the page will cause the browser to make a request to the server and the server will call our custom SessionSecurityTokenHandler where the ValidateSession method will throw an exception. Once this exception is thrown the request is stopped and all subsequent requests within the same session will have the same fate. The dead session should redirect to your login page.
To test this out all we have to do is load up our application and log in. Then if we create a new session by opening a new browser and logging in, e.g. switching from IE to Chrome, or within IE opening a new session via File > New Session, our first browser should notify you. If you click the End Session link you should automatically be logged out of the other session and redirected to your login page.
Pretty cool, huh?