Shaun Xu

The Sheep-Pen of the Shaun


News

logo

Shaun, the author of this blog is a semi-geek, clumsy developer, passionate speaker and incapable architect with about 10 years’ experience in .NET and JavaScript. He hopes to prove that software development is art rather than manufacturing. He's into cloud computing platform and technologies (Windows Azure, Amazon and Aliyun) and right now, Shaun is being attracted by JavaScript (Angular.js and Node.js) and he likes it.

Shaun is working at Worktile Inc. as the chief architect for overall design and develop worktile, a web-based collaboration and task management tool, and lesschat, a real-time communication aggregation tool.

MVP

My Stats

  • Posts - 122
  • Comments - 622
  • Trackbacks - 0

Tag Cloud


Recent Comments


Recent Posts


Archives


Post Categories


Image Galleries


.NET



Currently 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.

Comments

Gravatar # re: Set Context User Principal for Customized Authentication in SignalR
Posted by Rune Nesheim on 6/2/2014 8:11 PM
Thanks Shaun, I was looking for exactly this, and it works great!
Gravatar # How validate the token expire?
Posted by Sean on 6/11/2014 12:29 PM
In AuthorizeHubConnection, how to validate the token is valid (has expired or not)?
Gravatar # re: Set Context User Principal for Customized Authentication in SignalR
Posted by Shaun Xu on 6/12/2014 2:37 PM
@Sean
When you unproected the token you will got a ticket object which contain ExpireUTC property you can use.
Gravatar # re: Set Context User Principal for Customized Authentication in SignalR
Posted by Ong on 6/27/2014 6:23 PM
Hi Shaun Xu, thanks for your post.

I have a problem getting the Claims from Context.User.Identity in Hub.

It passed the authentication in AuthorizeHubConnection and I set request.Environment["server.User"] like the way you showed in this post.

This only happens after I upgraded Signalr version to 2.1.0. It was working fine for version 2.0.0.

Any idea ?
Gravatar # re: Set Context User Principal for Customized Authentication in SignalR
Posted by Shaun Xu on 6/28/2014 8:52 AM
@Ong

I didn't test with 2.1. Not sure if they changed the way the implementation of identity stuff.
Gravatar # re: Set Context User Principal for Customized Authentication in SignalR
Posted by Sameen on 7/11/2014 12:04 PM
How are you generating token, I have authenticated user in my client application, got authcookie and ticket. But how to get token out of it
Gravatar # re: Set Context User Principal for Customized Authentication in SignalR
Posted by Tamad on 11/2/2016 6:31 PM
Hi Shaun,
Thank you for the useful article.
Is there any link available, where the sample code can be downloaded ?
Gravatar # re: Set Context User Principal for Customized Authentication in SignalR
Posted by Bobble on 12/21/2016 2:17 AM
WebApiConfig.AuthenticationType what is this "AuthenticationType"
Post A Comment
Title:
Name:
Email:
Comment:
Verification: