Originally posted on: http://geekswithblogs.net/shaunxu/archive/2014/05/27/set-context-user-principal-for-customized-authentication-in-signalr.aspxCurrently I'm working on a single page application project which is built on AngularJS and ASP.NET WebAPI. When I need to implement some features that needs real-time communication and push notifications from server side I decided to use SignalR. SignalR is a project currently developed by Microsoft to build web-based, read-time communication application. You can find it here. With a lot of introductions and guides it's not a difficult task to use SignalR with ASP.NET WebAPI and AngularJS. I followed this and this even though it's based on SignalR 1. But when I tried to implement the authentication for my SignalR I was struggled 2 days and finally I got a solution by myself. This might not be the best one but it actually solved all my problem. In many articles it's said that you don't need to worry about the authentication of SignalR since it uses the web application authentication. For example if your web application utilizes form authentication, SignalR will use the user principal your web application authentication module resolved, check if the principal exist and authenticated. But in my solution my ASP.NET WebAPI, which is hosting SignalR as well, utilizes OAuth Bearer authentication. So when the SignalR connection was established the context user principal was empty. So I need to authentication and pass the principal by myself. Firstly I need to create a class which delivered from "AuthorizeAttribute", that will takes the responsible for authenticate when SignalR connection established and any method was invoked. 1: public class QueryStringBearerAuthorizeAttribute : AuthorizeAttribute
2: {
3: public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
4: {
5: }
6:
7: public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
8: {
9: }
10: }
The method "AuthorizeHubConnection" will be invoked when any SignalR connection was established. And here I'm going to retrieve the Bearer token from query string, try to decrypt and recover the login user's claims.
1: public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
2: {
3: var dataProtectionProvider = new DpapiDataProtectionProvider();
4: var secureDataFormat = new TicketDataFormat(dataProtectionProvider.Create());
5: // authenticate by using bearer token in query string
6: var token = request.QueryString.Get(WebApiConfig.AuthenticationType);
7: var ticket = secureDataFormat.Unprotect(token);
8: if (ticket != null && ticket.Identity != null && ticket.Identity.IsAuthenticated)
9: {
10: // set the authenticated user principal into environment so that it can be used in the future
11: request.Environment["server.User"] = new ClaimsPrincipal(ticket.Identity);
12: return true;
13: }
14: else
15: {
16: return false;
17: }
18: }
In the code above I created "TicketDataFormat" instance, which must be same as the one I used to generate the Bearer token when user logged in. Then I retrieve the token from request query string and unprotect it. If I got a valid ticket with identity and it's authenticated this means it's a valid token. Then I pass the user principal into request's environment property which can be used in nearly future.
Since my website was built in AngularJS so the SignalR client was in pure JavaScript, and it's not support to set customized HTTP headers in SignalR JavaScript client, I have to pass the Bearer token through request query string.
This is not a restriction of SignalR, but a restriction of WebSocket. For security reason WebSocket doesn't allow client to set customized HTTP headers from browser.
Next, I need to implement the authentication logic in method "AuthorizeHubMethodInvocation" which will be invoked when any SignalR method was invoked.
1: public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
2: {
3: var connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;
4: // check the authenticated user principal from environment
5: var environment = hubIncomingInvokerContext.Hub.Context.Request.Environment;
6: var principal = environment["server.User"] as ClaimsPrincipal;
7: if (principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)
8: {
9: // create a new HubCallerContext instance with the principal generated from token
10: // and replace the current context so that in hubs we can retrieve current user identity
11: hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId);
12: return true;
13: }
14: else
15: {
16: return false;
17: }
18: }
Since I had passed the user principal into request environment in previous method, I can simply check if it exists and valid. If so, what I need is to pass the principal into context so that SignalR hub can use. Since the "User" property is all read-only in "hubIncomingInvokerContext", I have to create a new "ServerRequest" instance with principal assigned, and set to "hubIncomingInvokerContext.Hub.Context". After that, we can retrieve the principal in my Hubs through "Context.User" as below.
1: public class DefaultHub : Hub
2: {
3: public object Initialize(string host, string service, JObject payload)
4: {
5: var connectionId = Context.ConnectionId;
6: ... ...
7: var domain = string.Empty;
8: var identity = Context.User.Identity as ClaimsIdentity;
9: if (identity != null)
10: {
11: var claim = identity.FindFirst("Domain");
12: if (claim != null)
13: {
14: domain = claim.Value;
15: }
16: }
17: ... ...
18: }
19: }
Finally I just need to add my "QueryStringBearerAuthorizeAttribute" into the SignalR pipeline.
1: app.Map("/signalr", map =>
2: {
3: // Setup the CORS middleware to run before SignalR.
4: // By default this will allow all origins. You can
5: // configure the set of origins and/or http verbs by
6: // providing a cors options with a different policy.
7: map.UseCors(CorsOptions.AllowAll);
8: var hubConfiguration = new HubConfiguration
9: {
10: // You can enable JSONP by uncommenting line below.
11: // JSONP requests are insecure but some older browsers (and some
12: // versions of IE) require JSONP to work cross domain
13: // EnableJSONP = true
14: EnableJavaScriptProxies = false
15: };
16: // Require authentication for all hubs
17: var authorizer = new QueryStringBearerAuthorizeAttribute();
18: var module = new AuthorizeModule(authorizer, authorizer);
19: GlobalHost.HubPipeline.AddModule(module);
20: // Run the SignalR pipeline. We're not using MapSignalR
21: // since this branch already runs under the "/signalr" path.
22: map.RunSignalR(hubConfiguration);
23: });
On the client side should pass the Bearer token through query string before I started the connection as below.
1: self.connection = $.hubConnection(signalrEndpoint);
2: self.proxy = self.connection.createHubProxy(hubName);
3: self.proxy.on(notifyEventName, function (event, payload) {
4: options.handler(event, payload);
5: });
6: // add the authentication token to query string
7: // we cannot use http headers since web socket protocol doesn't support
8: self.connection.qs = { Bearer: AuthService.getToken() };
9: // connection to hub
10: self.connection.start();
Hope this helps,
Shaun
All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.