As you know if you read my last post on hub authorization, when a new hub connection is created, the transport connection stays open whether or not the hub connection(s) are authorized. In my case, I am using SignalR in a carefully controlled, secure authenticated environment, and as a common-sense security measure, I wanted to just close down any connections to SignalR from random clients that had no business connecting to my servers. Unfortunately, as of 1.0 RC1 there’s no built-in way to do this, but with some quick pointers from David Fowler I was able to put together something with Owin that causes my code to be run on every request first, before handing it off to SignalR. The result seemed like it might be useful enough to other people to be worthy of a blog post.
Disclaimers: This code was written for SignalR 1.0 RC1 and the version of owin that was around at that time. A visitor has posted some updates in the comments on this post’s page for SignalR 1.1.3. This applies to SignalR on top of IIS; I don’t know how it would be different if you’re hosting in some other way. This applies to requests that are routed to SignalR hubs (hub transport connections, method invocations, hubs auto-javascript.) You may need to do a similar thing for Persistent Connections too if you use them. Also, most of the calls and callbacks to and from Owin are just what “seemed to work,” but that doesn’t mean this is the best or most correct possible way to do it. We’ll just have to wait for Owin to be documented for that.
Step 1: Ditch RouteTable.Routes.MapHubs();
You should recognize this call from your application startup code (if you added SignalR via nuget, it’s in App_Start/RegisterHubs.cs). Internally, MapHubs tells Owin to route requests with certain urls right into SignalR code. This isn’t how we want to route. What we want to do is route them to our code first, where we’ll decide if we want to let the request through to SignalR or close it. So, we have to do do some routing calls ourselves:
// in addition to the usings you already have,
using Microsoft.AspNet.SignalR.SystemWeb.Infrastructure;
using Microsoft.Owin.Host.SystemWeb;
using Owin; // actually defined in a SignalR assembly and probably going to change soon...
public static void Start()
{
// RouteTable.Routes.MapHubs(); not doing this anymore
MapHubsWithSecurityInspector(RouteTable.Routes, "~/signalr", GlobalHost.DependencyResolver);
}
private static RouteBase MapHubsWithSecurityInspector(RouteCollection routes, string url, IDependencyResolver resolver)
{
var existing = routes["signalr.hubs"];
if (existing != null)
{
.Remove(existing);
routes}
var routeUrl = url.TrimStart('~').TrimStart('/');
var locator = new Lazy(() => new BuildManagerAssemblyLocator());
.Register(typeof(IAssemblyLocator), () => locator.Value);
resolver
return routes.MapOwinRoute("signalr.hubs", routeUrl, map => OwinSetup(map, resolver));
}
private static void OwinSetup(IAppBuilder builder, IDependencyResolver resolver)
{
.Use(typeof(MySecurityInspectionHandler));
builder
.MapHubs(resolver);
builder}
Step 2: Write MySecurityInspectionHandler
After you’ve replaced MapHubs with the above, Owin will need you to define a MySecurityInspectionHandler class, because it will try to call it on every incoming request to routeUrl. The signature of MySecurityInspectionHandler is a bit magical, as the constructor and callback Owin actually looks for isn’t expressed anywhere in docs or as an interface or anything. Here’s what seems to work:
using System.Threading.Tasks;
// u might want to put the rest in your app's namespace...
using AppFunc = Func<IDictionary<string, object>, Task>;
public class MySecurityInspectionHandler
{
private AppFunc _app;
public MySecurityInspectionHandler(AppFunc app)
{
= app;
_app }
public Task Invoke(IDictionary<string, object> environment)
{
// Finally, here's where we can examine the incoming request and allow or reject it
if(RequestHasRightCookiesOrSomething(environment))
{
// continue processing the request
return _app.Invoke(environment);
} else {
return StopRequestProcessing(environment);
}
}
private Task StopRequestProcessing(IDictionary<string, object> environment)
{
.Add(ResponseStatusCode, 403);
environment
object responseStreamObj;
.TryGetValue(ResponseBody, out responseStreamObj);
environment= (Stream)responseStreamObj;
Stream responseStream
var streamWriter = new StreamWriter(responseStream);
.Write("403 Forbidden.");
streamWriter.Close();
streamWriter
var tcs = new TaskCompletionSource<bool>();
.SetResult(false);
tcsreturn tcs.Task;
}
}
Then all you have to do is implement your actual logic to detect if this is a request from an authorized source or not, in a RequestHasRightCookiesOrSomething method. Just about everything you could ever want to know about the incoming request can be found in that environment IDictionary, unfortunately as objects with string keys that Intellisense couldn’t help you with. When you get to this stage just shove a breakpoint in and take a look at the Keys / Values inside environment to find what you need.
Step 3: Test
Visit some urls to your SignalR hubs manually in a browser, both with and without the expected authentication features sent along. When you do not send the authentication information, you should get a 403 forbidden back from URLs like
- http://localhost/myproj/signalr
- http://localhost/myproj/signalr/hubs
- http://localhost/myproj/signalr/anything_else
When you do send authentication, /signalr visited manually currently produces an unknown transport exception, and /signalr/hubs produces the auto-javascript.
Hope this helps. Happy routing!