24

+i used this solution to implement Token Based Authentication using ASP.NET Web API 2, Owin, and Identity...which worked out excellently well. i used this other solution and this to implement signalR hubs authorization and authentication by passing the bearer token through a connection string, but seems like either the bearer token is not going, or something else is wrong somewhere, which is why am here seeking HELP...these are my codes... QueryStringBearerAuthorizeAttribute: this is the class in charge of verification

using ImpAuth.Entities;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;

namespace ImpAuth.Providers
{
    using System.Security.Claims;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    using Microsoft.AspNet.SignalR.Owin;

    public class QueryStringBearerAuthorizeAttribute : AuthorizeAttribute
    {
        public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
        {
            var token = request.QueryString.Get("Bearer");
            var authenticationTicket = Startup.AuthServerOptions.AccessTokenFormat.Unprotect(token);

            if (authenticationTicket == null || authenticationTicket.Identity == null || !authenticationTicket.Identity.IsAuthenticated)
            {
                return false;
            }

            request.Environment["server.User"] = new ClaimsPrincipal(authenticationTicket.Identity);
            request.Environment["server.Username"] = authenticationTicket.Identity.Name;
            request.GetHttpContext().User = new ClaimsPrincipal(authenticationTicket.Identity);
            return true;
        }

        public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
        {
            var connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;

            // check the authenticated user principal from environment
            var environment = hubIncomingInvokerContext.Hub.Context.Request.Environment;
            var principal = environment["server.User"] as ClaimsPrincipal;

            if (principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)
            {
                // create a new HubCallerContext instance with the principal generated from token
                // and replace the current context so that in hubs we can retrieve current user identity
                hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId);

                return true;
            }

            return false;
        }
    }
}

and this is my start up class....

using ImpAuth.Providers;
using Microsoft.AspNet.SignalR;
using Microsoft.Owin;
using Microsoft.Owin.Cors;
using Microsoft.Owin.Security.Facebook;
using Microsoft.Owin.Security.Google;
//using Microsoft.Owin.Security.Facebook;
//using Microsoft.Owin.Security.Google;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Http;

[assembly: OwinStartup(typeof(ImpAuth.Startup))]

namespace ImpAuth
{
    public class Startup
    {
        public static OAuthAuthorizationServerOptions AuthServerOptions;

        static Startup()
        {
            AuthServerOptions = new OAuthAuthorizationServerOptions
            {
                AllowInsecureHttp = true,
                TokenEndpointPath = new PathString("/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
                Provider = new SimpleAuthorizationServerProvider()
               // RefreshTokenProvider = new SimpleRefreshTokenProvider()
            };
        }

        public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; }
        public static GoogleOAuth2AuthenticationOptions googleAuthOptions { get; private set; }
        public static FacebookAuthenticationOptions facebookAuthOptions { get; private set; }

        public void Configuration(IAppBuilder app)
        {
            //app.MapSignalR();
            ConfigureOAuth(app);
            app.Map("/signalr", map =>
            {
                // Setup the CORS middleware to run before SignalR.
                // By default this will allow all origins. You can 
                // configure the set of origins and/or http verbs by
                // providing a cors options with a different policy.
                map.UseCors(CorsOptions.AllowAll);
                var hubConfiguration = new HubConfiguration
                {
                    // You can enable JSONP by uncommenting line below.
                    // JSONP requests are insecure but some older browsers (and some
                    // versions of IE) require JSONP to work cross domain
                    //EnableJSONP = true
                    EnableDetailedErrors = true
                };
                // Run the SignalR pipeline. We're not using MapSignalR
                // since this branch already runs under the "/signalr"
                // path.
                map.RunSignalR(hubConfiguration);
            });
            HttpConfiguration config = new HttpConfiguration();
            app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
            WebApiConfig.Register(config);
            app.UseWebApi(config);
        }

        public void ConfigureOAuth(IAppBuilder app)
        {
            //use a cookie to temporarily store information about a user logging in with a third party login provider
            app.UseExternalSignInCookie(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ExternalCookie);
            OAuthBearerOptions = new OAuthBearerAuthenticationOptions();

            OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
            {
                AllowInsecureHttp = true,
                TokenEndpointPath = new PathString("/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
                Provider = new SimpleAuthorizationServerProvider()
            };

            // Token Generation
            app.UseOAuthAuthorizationServer(OAuthServerOptions);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

            //Configure Google External Login
            googleAuthOptions = new GoogleOAuth2AuthenticationOptions()
            {
                ClientId = "1062903283154-94kdm6orqj8epcq3ilp4ep2liv96c5mn.apps.googleusercontent.com",
                ClientSecret = "rv5mJUz0epWXmvWUAQJSpP85",
                Provider = new GoogleAuthProvider()
            };
            app.UseGoogleAuthentication(googleAuthOptions);

            //Configure Facebook External Login
            facebookAuthOptions = new FacebookAuthenticationOptions()
            {
                AppId = "CHARLIE",
                AppSecret = "xxxxxx",
                Provider = new FacebookAuthProvider()
            };
            app.UseFacebookAuthentication(facebookAuthOptions);
        }
    }

}

and this is the knockout plus jquery code on the client....

function chat(name, message) {
    self.Name = ko.observable(name);
    self.Message = ko.observable(message);
}

function viewModel() {
    var self = this;
    self.chatMessages = ko.observableArray();

    self.sendMessage = function () {
        if (!$('#message').val() == '' && !$('#name').val() == '') {
            $.connection.hub.qs = { Bearer: "yyCH391w-CkSVMv7ieH2quEihDUOpWymxI12Vh7gtnZJpWRRkajQGZhrU5DnEVkOy-hpLJ4MyhZnrB_EMhM0FjrLx5bjmikhl6EeyjpMlwkRDM2lfgKMF4e82UaUg1ZFc7JFAt4dFvHRshX9ay0ziCnuwGLvvYhiriew2v-F7d0bC18q5oqwZCmSogg2Osr63gAAX1oo9zOjx5pe2ClFHTlr7GlceM6CTR0jz2mYjSI" };
            $.connection.hub.start().done(function () {
                $.connection.hub.qs = { Bearer: "yyCH391w-CkSVMv7ieH2quEihDUOpWymxI12Vh7gtnZJpWRRkajQGZhrU5DnEVkOy-hpLJ4MyhZnrB_EMhM0FjrLx5bjmikhl6EeyjpMlwkRDM2lfgKMF4e82UaUg1ZFc7JFAt4dFvHRshX9ay0ziCnuwGLvvYhiriew2v-F7d0bC18q5oqwZCmSogg2Osr63gAAX1oo9zOjx5pe2ClFHTlr7GlceM6CTR0jz2mYjSI" };
                $.connection.impAuthHub.server.sendMessage($('#name').val(), $('#message').val())
                            .done(function () { $('#message').val(''); $('#name').val(''); })
                            .fail(function (e) { alert(e) });
            });
        }
    }

    $.connection.impAuthHub.client.newMessage = function (NAME, MESSAGE) {
        //alert(ko.toJSON(NAME, MESSAGE));
        var chat1 = new chat(NAME, MESSAGE);
        self.chatMessages.push(chat1);
    }

}

ko.applyBindings(new viewModel());

and here is my hub class...

using ImpAuth.Providers;
using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace ImpAuth
{
    public class impAuthHub : Hub
    {
        [QueryStringBearerAuthorize]
        public void SendMessage(string name, string message)
        {

            Clients.All.newMessage(name, message);
        }
    }
}

...now the problem comes when i try to invoke an authenticated hub class and i get this error

caller is not authenticated to invove method sendMessage in impAuthHub

but then i change this method in QueryStringBearerAuthorizeAttribute class to alway return true like this

public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
{
    var connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;
    // check the authenticated user principal from environment
    var environment = hubIncomingInvokerContext.Hub.Context.Request.Environment;
    var principal = environment["server.User"] as ClaimsPrincipal;

    if (principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)
    {
        // create a new HubCallerContext instance with the principal generated from token
        // and replace the current context so that in hubs we can retrieve current user identity
        hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId);

        return true;
    }

    return true;
}

...it works....WHAT IS THE PROBLEM WITH MY CODE OR IMPLEMENTATION?

johnny 5
  • 19,893
  • 50
  • 121
  • 195
McKabue
  • 2,076
  • 1
  • 19
  • 34
  • I've sent an email Louis who forked my repo and implemented the integration with SignalR, hopefully he will check and be able to help. Glad that my posts was useful in your case :) – Taiseer Joudeh Oct 30 '14 at 21:55
  • Hi, Taiseer thanks for the email. McKabue, there are a few things that I can think of. first off, could you possibly debug your application and break it on the line where we are setting var principal. I would like to see what values are placed in principal. This would be the best place to start. – Louis Lewis Oct 30 '14 at 23:39
  • +hello Lewis...i am getting a null principle value...either the bearer token isn't being sent, buy the way, how do i check if the bearer token is being sent from the client? – McKabue Oct 31 '14 at 09:43
  • +@Louis-Lewis this method 'public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request){}' doesn't seem to be invoked at any given point...why? is that supposed to be it? – McKabue Oct 31 '14 at 10:10
  • A quick initial look, would be that you have assigned the attribute on the method and not on the class, I would say that is why the AuthorizeHubConnection method is not firing. I would suggest as a starting point, try move the attribute over the class and then we take things from there. – Louis Lewis Oct 31 '14 at 12:54
  • To see whether or not the token is received at all, you could take a look at line 1 in this method: AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request). The variable called token should be set with the received value that was sent by the client. – Louis Lewis Oct 31 '14 at 12:57
  • In your start up class you are also creating a new OAuthAuthorizationServerOptions variable. Where there is a global one that is initialised in the constructor of the startup class. change this line app.UseOAuthAuthorizationServer(OAuthServerOptions); to app.UseOAuthAuthorizationServer(AuthServerOptions); – Louis Lewis Oct 31 '14 at 13:07
  • +thanks lewis, it worked out well when i moved the attribute over the hub class, but its not returning a false response when bearer token is wrong or unavailable as it did earlier...i want to catch the false response in the fail method { .fail(function (e) { alert(e) });} so i can prompt the client for login... – McKabue Nov 01 '14 at 10:39
  • I am happy to hear that you got it working. Now for you fail method, you are going to have to do things a little bit different. You will not see the "false" value being returned, that "false" value is rather used by the SignalR code internally. the best thing I can recommend is you implement SignalR client error handling. A example can be found here. http://www.asp.net/signalr/overview/guide-to-the-api/hubs-api-guide-javascript-client#handleerrors Let me know if you don't come right. – Louis Lewis Nov 01 '14 at 15:01

3 Answers3

41

You need to configure your signalr like this;

app.Map("/signalr", map =>
{
    map.UseCors(CorsOptions.AllowAll);

    map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
    {
        Provider = new QueryStringOAuthBearerProvider()
    });

    var hubConfiguration = new HubConfiguration
    {
        Resolver = GlobalHost.DependencyResolver,
    };
    map.RunSignalR(hubConfiguration);
});

Then you need to write a basic custom OAuthBearerAuthenticationProvider for signalR which accepts access_token as query string.

public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
    public override Task RequestToken(OAuthRequestTokenContext context)
    {
        var value = context.Request.Query.Get("access_token");

        if (!string.IsNullOrEmpty(value))
        {
            context.Token = value;
        }

        return Task.FromResult<object>(null);
    }
}

After this all you need is to send access_token with signalr connection as querystring.

$.connection.hub.qs = { 'access_token': token };

And for your hub just ordinary [Authorize] attribute

public class impAuthHub : Hub
{
    [Authorize]
    public void SendMessage(string name, string message)
    {
       Clients.All.newMessage(name, message);
    }
}

Hope this helps. YD.

Peter
  • 3,916
  • 1
  • 22
  • 43
Yusuf Demirag
  • 773
  • 7
  • 10
  • And how would you access the user from the hub `Context`? Are you able to call `Context.User.Identity.Name`? What does this line exactly do? `context.Token = value;` How does it populate the hub context? Thanks! – radu-matei Sep 03 '15 at 12:57
  • 7
    I just tried this method , but unfortunately my, `Context.User.Identity.Name` is `null` – radu-matei Sep 03 '15 at 13:02
  • 1
    Where does the `context` from the `Provider` fits in the OWIN pipeline? How will the fact that I set `context.Token` to the token retrieved from the client influence my `Context`? Any repo/more complex example would be gold for me right now. Thanks! – radu-matei Sep 03 '15 at 13:11
  • @radu-matei, did you ever figure this out? – tofutim Sep 30 '16 at 04:43
  • I wonder if this is still needed since you are able to stick the Bearer into headers via conn.Headers["x-zumo-auth"] = _appService.CurrentUser.MobileServiceAuthenticationToken; – tofutim Sep 30 '16 at 04:46
  • 1
    @tofutim I actually createad a repo for this, [you can find it here](https://github.com/microsoft-dx/aspnet-fundamentals/tree/master/AspNetFundamentals/aspnet04%20-%20SimpleTokenAuthentication). – radu-matei Sep 30 '16 at 08:06
  • I got it to work in my C# app by looking at the header, but I probably need your query version if web sockets is used. Is that right? – tofutim Oct 03 '16 at 05:03
  • @tofutim yes that is right. You need to configure query string version for websockets – Yusuf Demirag Oct 25 '16 at 15:04
  • strangely websockets seems to work with the header tokens – tofutim Oct 25 '16 at 15:52
  • This was exactly what I needed. Thanks so much! – Brendan Nov 06 '16 at 19:09
  • @radu-matei That repo is really useful, thanks! I'm curious, though - in `index.html`, how would I get the value for the 'BearerToken' variable that you're passing in the query string? I think I've implemented everything else correctly, but I just don't know where to get that value from or how to do so in my Javascript client code. – Philip Stratford Mar 07 '17 at 15:49
  • Basically get the token from PostMan (making a request to `/token`, then send it through the method of your choice – radu-matei Mar 08 '17 at 16:17
  • This is really great! I wonder why there isn't such article in MSDN. They only explain about .NET clients, but I'm sure that web clients are the majority. – luizs81 Oct 24 '17 at 01:30
  • You really save my day ! I can even get the ID of the user with Identity.GetUserId () ! Awesome Thx – Vincenzo Nov 26 '17 at 19:54
  • I try your solution and download your repo but get error 401 Unauthorized. Hope you can help me – Shalom Dahan Jan 15 '18 at 16:05
  • 2
    @radu-matei please can you check the repo, link is broken - I see on the repo the project was restructured. Is [this](https://github.com/microsoft-dx/aspnet-fundamentals/tree/master/aspnet04%20-%20SimpleTokenAuthentication) the correct link? – Johan Aspeling Mar 04 '20 at 07:05
  • @JohanAspeling I followed this. https://www.infozone.se/en/blog/2016/03/21/authenticate-against-signalr-2/ – SillyPerson Mar 06 '20 at 12:23
3

Can't comment so adding my answer after the comments on Peter's excellent answer.

Did a bit more digging and the user id that I had set in my custom owin authorization provider was hiding here (complete hub method shown).

    [Authorize]
    public async Task<int> Test()
    {
        var claims = (Context.User.Identity as System.Security.Claims.ClaimsIdentity).Claims.FirstOrDefault();
        if (claims != null)
        {
            var userId = claims.Value;

            //security party!
            return 1;
        }

        return 0;
    }

More added for texas697:

Startup.Auth.cs add this to ConfigureAuth() if not already there:

app.Map("/signalr", map =>
    {
        map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
        {
            Provider = new QueryStringOAuthBearerProvider() //important bit!
        });

        var hubConfiguration = new HubConfiguration
        {
            EnableDetailedErrors = true,
            Resolver = GlobalHost.DependencyResolver,
        };
        map.RunSignalR(hubConfiguration);
    });

The custom auth provider looks like this:

public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
    public override Task RequestToken(OAuthRequestTokenContext context)
    {
        var value = context.Request.Query.Get("access_token");

        if (!string.IsNullOrEmpty(value))
        {
            context.Token = value;
        }

        return Task.FromResult<object>(null);
    }
}
Community
  • 1
  • 1
deejbee
  • 1,148
  • 11
  • 17
  • 2
    Did this work? I am getting Error: Caller is not authorized to invoke the method on . I can see that the Request token is executed correctly and the token is assigned with the right value. – Deepak Sharma Sep 06 '17 at 13:37
2

I followed this:

first Add the JWT to the query string:'

this.connection = $['hubConnection']();
this.connection.qs = { 'access_token': token}

then in thestartup.cs,before JwtBearerAuthentication, add the token to the header:

 app.Use(async (context, next) =>
            {
                if (string.IsNullOrWhiteSpace(context.Request.Headers["Authorization"]) && context.Request.QueryString.HasValue)
                {
                    var token = context.Request.QueryString.Value.Split('&').SingleOrDefault(x => x.Contains("access_token"))?.Split('=')[1];
                    if (!string.IsNullOrWhiteSpace(token))
                    {
                        context.Request.Headers.Add("Authorization", new[] { $"Bearer {token}" });
                    }
                }
                await next.Invoke();
            });


            var keyResolver = new JwtSigningKeyResolver(new AuthenticationKeyContainer());
            app.UseJwtBearerAuthentication(
                new JwtBearerAuthenticationOptions
                {
                    AuthenticationMode = AuthenticationMode.Active,
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = ConfigurationUtil.ocdpAuthAudience,
                        ValidIssuer = ConfigurationUtil.ocdpAuthZero,
                        IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => keyResolver.GetSigningKey(kid)
                    }
                });


            ValidateSignalRConnectionData(app);
            var hubConfiguration = new HubConfiguration
            {
                EnableDetailedErrors = true
            };
            app.MapSignalR(hubConfiguration);
SillyPerson
  • 589
  • 2
  • 7
  • 30