AzureSignalR-samples icon indicating copy to clipboard operation
AzureSignalR-samples copied to clipboard

Not getting hub OnConnected/OnDisconnectedAsync calls

Open johnkwaters opened this issue 6 years ago • 62 comments

I switched from SignalR (AspNetCore RC1) to AzureSignalR I use authenticated hubs (using JWT Bearer tokens in header) When a client connects, I add the connection to a group for its Tenant (a multitenant solution, tenant is a claim in the JWT). However - in the AzureSignalR version, my hub does not get these events.

My code defines public override async Task OnConnectedAsync()

I noticed the sample code in FlightMap does not use async, public override Task OnConnectedAsync()

Changing my code to use this signature still doesnt work.

Found the angular client was requesting HttpTransportType.WebSockets. When removing that ... it works!

johnkwaters avatar May 08 '18 20:05 johnkwaters

I am a little confused. So it works now?

xscript avatar May 14 '18 01:05 xscript

Are you saying you don't get any connection events when using websockets and the azure signalr service?

davidfowl avatar May 14 '18 01:05 davidfowl

I found that if the client specifies HttpTransportType WebSockets, I dont get any connection events. If I leave it unspecified, I do.

johnkwaters avatar May 14 '18 15:05 johnkwaters

Thats extremely odd. Did you look at the logs on the server and client?

davidfowl avatar May 14 '18 18:05 davidfowl

Mu code is kind of in ripped up floorboards state right now. I will check it again in the next few days and let you know what I see. Sorry for the delay.

johnkwaters avatar May 14 '18 18:05 johnkwaters

So got back into this now.. if I use local SignalR, it works, if I use Azure SignalR it doesnt. The hub is authorized, i.e. it has an [Authorize] attribute. The angular client calls a login API, gets a JWT, then sends this on the connect call. I see it coming though, and I have a middleware that places it in the Authorization HTTP header, with Bearer . The JWT has some claims, including the TenantId of the client. So when the OnConnectedAsync is called, I can get the Tenant of the caller from the claim and I add the connection to a Group for that Tenant. However, when I switch to Azure SignalR, I no longer get the OnConnectedAsync call. So it has something to do with the Hub being [Authorize] decorated. I see their are some options in the AddAzureSignalR for a claims provider.... do I use that?

Note the original text above talks about websockets, but this is happening no matter what. The angualr client is now using

"@aspnet/signalr": "^1.0.0-rc1-update1",

The client connection logic looks like this;

`import {EventEmitter} from "@angular/core"; import {each} from 'lodash-es'; import {AppConstants} from "../../constants/app.constants"; import {HubEvent} from "../../models/hub-event.model"; import {SessionData} from "../../interfaces/api/session-data.interface"; import {AppSession} from "../core/app-session.service"; import {HubConnection, HttpTransportType, HubConnectionBuilder} from "@aspnet/signalr";

export abstract class BaseHub { eventBus: EventEmitter<HubEvent> = new EventEmitter<HubEvent>();

private _hubConnection: HubConnection; private _hubName: string; private _events: Array;

constructor(hubName: string, events: Array) { const sessionData: SessionData = AppSession.get();

this._hubName = hubName;
this._events = events;

this._hubConnection = new HubConnectionBuilder()
.withUrl(`${AppConstants.signalR.baseUrl}/${this._hubName}?token=${sessionData.token}`)
.build();

this.subscribeToEvents();

this._hubConnection.start();

}

invoke(hubEvent: HubEvent) { this._hubConnection.invoke(hubEvent.name, hubEvent.data); }

private subscribeToEvents(): void { each(this._events, (event) => { this.setupHubEventListener(event); }); }

private setupHubEventListener(eventName: string) { const evt = eventName; this._hubConnection.on(evt, (data: any) => { this.eventBus.emit( new HubEvent(evt, data) ); }); } }`

The server is a dotnetcore app, target framework netcoreapp2.1, with these packages <PackageReference Include="AutoMapper" Version="6.2.2" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.2.0" /> <PackageReference Include="EPPlus" Version="4.5.1" /> <PackageReference Include="GeoTimeZone" Version="3.2.0" /> <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.2.1" /> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.0-rc1-final" /> <PackageReference Include="Microsoft.Azure.SignalR" Version="1.0.0-preview1-10009" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.1.0-rc1-final" /> <PackageReference Include="NodaTime" Version="2.3.0" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="2.4.0" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="2.4.0" /> The startup code is

` public void ConfigureServices(IServiceCollection services) { services .AddOptions() .AddConfig(ConfigurationRoot) .AddCORS() .AddEF(ConfigurationRoot) .ConfigureApplicationInjection() .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { var tokenOptions = ConfigurationRoot.GetSection("Authentication").Get<TokenOptions>();

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = tokenOptions.Issuer,
                    ValidAudience = tokenOptions.Audience,
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(tokenOptions.SigningKey))
                };
            });
        services
            .AddMvc()
            .AddJsonOptions(opt =>
            {
                opt.SerializerSettings.NullValueHandling = NullValueHandling.Include;
                opt.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include;
                opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            });

#if !AZURESIGNALR services.AddSignalR(); #else var signalRConnectionString = ConfigurationRoot.GetSection("Azure:SignalR").GetValue("ConnectionString"); services.AddSignalR().AddAzureSignalR(signalRConnectionString); #endif services.AddSwagger(); services.AddAutoMapper(); } `

And

`
public void Configure(IApplicationBuilder app) { app .UseTokenInQueryString() .UseAuthentication() .UseConfigureSession() .UseSwagger( c => { c.PreSerializeFilters.Add( (swagger, httpReq) => swagger.Host = httpReq.Host.Value); }) .UseSwaggerUI(c => { // when running in a Docker container this is null and needs adjusting var basePath = Environment.GetEnvironmentVariable("ASPNETCORE_APPL_PATH") ?? "/"; c.SwaggerEndpoint(basePath + "swagger/v1/swagger.json", "V1 Docs"); c.DocExpansion(DocExpansion.None); }) .UseDeveloperExceptionPage() .UseHttpException() .UseCors("AllowAllCorsPolicy") .UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }) #if !AZURESIGNALR .UseSignalR( #else .UseAzureSignalR( #endif routes => { routes.MapHub<StopsHub>("/stophub"); });

        var config = new MapperConfiguration(cfg => {
            cfg.AddProfile<MappingProfile>();
        });
    }

` My CORS policy is

public static IServiceCollection AddCORS(this IServiceCollection services) { services.AddCors(action => action.AddPolicy("AllowAllCorsPolicy", builder => builder .AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials())); return services; }

Here is my middleware that moves the token from the query string to the header

` public class GetTokenFromQueryStringMiddleware { private readonly RequestDelegate _next;

    /// <summary>
    /// constructor
    /// </summary>
    /// <param name="next">the next middleware in chain</param>
    public GetTokenFromQueryStringMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    /// <summary>
    /// sets the header
    /// </summary>
    /// <param name="context">Current HTTP context</param>
    /// <returns></returns>
    public async Task InvokeAsync(HttpContext context)
    {
        if (string.IsNullOrWhiteSpace(context.Request.Headers["Authorization"]))
        {
            if (context.Request.QueryString.HasValue)
            {
                var token = context.Request.QueryString.Value.Split('&')
                    .SingleOrDefault(x => x.Contains("token"))?.Split('=')[1];
                if (!string.IsNullOrWhiteSpace(token))
                {
                    context.Request.Headers.Add("Authorization", new[] { $"Bearer {token}" });
                }
            }
        }
        await _next.Invoke(context);
    }
}`

Sorry about the formatting, that add code feature works so so!

johnkwaters avatar May 23 '18 16:05 johnkwaters

I found it I remove the Authorize attribute on my Hub it works. But of course, I dont want to do that.

I am looking at the wss connection to the Azure SignalR service from the angular client

wss://northstardevsignalr.service.signalr.net:5001/client/?hub=stopshub&id=5N_GqVK2qAhTz1lagtMjNQ&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOjEzLCJ1c2VybmFtZSI6ImFyY3RpY0Bub3J0aHN0YXIuYXBwIiwidGVuYW50aWQiOjUsInJvbGUiOiJUZW5hbnRBZG1pbiIsImV4cCI6MTUyNzEzNDM0OCwiaXNzIjoibm9ydGhzdGFyYXBpIiwiYXVkIjpbIm5vcnRoc3RhciIsImh0dHBzOi8vbm9ydGhzdGFyZGV2c2lnbmFsci5zZXJ2aWNlLnNpZ25hbHIubmV0OjUwMDEvY2xpZW50Lz9odWI9c3RvcHNodWIiXSwibmJmIjoxNTI3MTMwNzQ4LCJpYXQiOjE1MjcxMzA3NDh9.ZBnidJ4qAPceXFTrUcbIr_cmxMCpunxqP0P_s1zvR50

It looks like it is passing the JWT sent in my connect call over to Azure SignalR. Wondering if maybe then Azure SignalR somehow uses this when connecting back to my hub. But I dont know how...and however it does it is maybe not passing my Authorize requirement?

johnkwaters avatar May 24 '18 03:05 johnkwaters

The Authorize requirement still runs in your app server. By default, all claims of an Authenticated user will be passed to SignalR Service. Then SignalR Service will pass all claims in the JWT back to your app server to build a new connection. From the token you pasted here, I can see tenantid claim exists and has the value of 5. I am trying to figure out what is the gap here.

xscript avatar May 24 '18 07:05 xscript

Thanks! To sum up the findings so far: when using Authorize and SignalR Azure OnConnectedAsync is not called. When using plain vanilla signalr, it is. If I remove Authorize, it is called. The authorization itself appears to be working fine, the tokens flying around in the logs look correct. One more observation - I tried adding a handler for the getclaims function in the Azure SignalR options, and could see that the claims principal passed into that func already had the correct claims.

johnkwaters avatar May 24 '18 13:05 johnkwaters

"Then SignalR Service will pass all claims in the JWT back to your app server to build a new connection" - how does it pass them back - the way it does this seems to not pass the Authorize muster. My app is happy if it finds the JWT on the querystring in a variable called token, or access_token, or if it is in the Authorization: Bearer http header. How does the service pass the claims when building the new connection?

johnkwaters avatar May 24 '18 13:05 johnkwaters

And looking at the debug console I dont see any token authentication failing. I dont see any failures at all. On the middle tier or the client.

johnkwaters avatar May 24 '18 13:05 johnkwaters

I looked at the token too and it has all the correct claims in it.

johnkwaters avatar May 24 '18 13:05 johnkwaters

This is pretty weird. We have a sample using authorized hub too. I tried it again today and it works well. You can find the code here. I changed it a little to use a policy. Client can successfully connected to server.

xscript avatar May 24 '18 13:05 xscript

Its not the same auth scheme though. You are using github and cookies... my auth pipeline uses JwtBearer

johnkwaters avatar May 24 '18 13:05 johnkwaters

How about you see if your code works with a JWT and Bearer Auth, no cookies or GitHub oauth? Also - how does the Azure SignalR service establish it's connection? I dont see that incoming request...

johnkwaters avatar May 25 '18 00:05 johnkwaters

Sure. I am trying out the JWT scheme.

To answer your second question, there is a protocol between Service and your app server. For every new client connected to Service, a OpenConnectionMessage will be sent from Service to your app server. https://github.com/Azure/azure-signalr/blob/dev/specs/ServiceProtocol.md#new-client-connect

xscript avatar May 25 '18 14:05 xscript

Hey @johnkwaters , I tried JWT scheme and it still works on my side. You can find the code at here. It provides a public API for client to get JWT token and use this token to access the Chat hub.

You can use this sample as a starting point and add your authorization logic to locate the issue. If things still don't work out for you, let's schedule a time convenient for both of us and debug it together.

xscript avatar May 26 '18 12:05 xscript

OK, thanks a million, will review and compare!

John

johnkwaters avatar May 26 '18 15:05 johnkwaters

Kevin,

Is it significant that you have UseFileServer in there? Why is that?

John

johnkwaters avatar May 29 '18 18:05 johnkwaters

So I see one thing: in your HTML client, you build the connection using an accessTokenFactory?

        var connection = new signalR.HubConnectionBuilder()
            .withUrl('/chat', {
                    accessTokenFactory: () => accessToken
                })
            .build();

I have an Angular client, and it looks like this:

  this._hubConnection = new HubConnectionBuilder()
    .withUrl(`${AppConstants.signalR.baseUrl}/${this._hubName}?token=${sessionData.token}`)
    .build();

The token is passed on the query string. These seem to be different approaches. How do I do the accessTokenFactory thing in Angular?

I see there is an overload of withUrl that takes an IHttpConnectionOptions, but how do I use that?

image

This seems to work...but same problem. With AzureSignalR, I still don't get the OnConnected event.

  this._hubConnection = new HubConnectionBuilder()
    .withUrl(
      `${AppConstants.signalR.baseUrl}/${this._hubName}`,
      {
        accessTokenFactory: () => sessionData.token
      })
    .build();

johnkwaters avatar May 29 '18 18:05 johnkwaters

I guess one other difference - or two - I am using HTTPS, and also, my Angular client is not served up from the same site as the API (your client is), so I have CORS in affect. I also see your client has a dependency on msgpack?

"@aspnet/signalr-protocol-msgpack": "^1.0.0-rc1-final"

johnkwaters avatar May 29 '18 19:05 johnkwaters

I tried to get your example to work with the HTML client in a separate project, with CORS on the API/Hub project. It authenticated but the SignalR part didnt work. Could you try to refine your example to separate the two? Even better - to have an angular client?

In my Startup code I have this in ConfigureServices:

        services.AddCors(action =>
            action.AddPolicy("AllowAllCorsPolicy", 
                builder =>
                    builder
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        .AllowAnyOrigin()
                        .AllowCredentials()));

And in Configure, I have

        app
            .UseCors("AllowAllCorsPolicy")

johnkwaters avatar May 29 '18 22:05 johnkwaters

Just touching bases on this - it is still a problem for us. We have tried emulating everything in your sample, including the access token factory, same negative results. The only thing different I can see of any significance is that your example the client connects to the hub served from the same domain, no CORS, but in our case, we have an angular client and CORS set up for our API. Would you mind trying this in your sample? Preferably with an Angular client?

johnkwaters avatar Jun 08 '18 00:06 johnkwaters

Hey @johnkwaters , sorry to know that you are still blocked. So do you have a sample to reproduce this issue? Given my limited experience of Angular and limited bandwidth, it would be faster to start with an existing project.

xscript avatar Jun 08 '18 01:06 xscript

Hi I have faced the exact same issue. I am not on Angular. I believe JavaScript framework doesn't matter. The combination that triggers this issue are following:

  • SignalR running on a different API server (different host name)
  • CORS is in effect
  • ASP.net core 2.1 Web API server (Kestrel)
  • Client uses JWT bearer token (passing via query string with access token factory and middle ware that caches the access token from query string)
  • Azure SignalR service is used
  • There is Authorize attribute on Hub

When the above conditions are met, the onConnected events are not invoked. This one is really blocking us. :-(

MoimHossain avatar Oct 18 '18 18:10 MoimHossain

I’m confused. If authentication fails, the hub won’t be activated. That’s by design.

davidfowl avatar Oct 18 '18 20:10 davidfowl

The problem was that the OnConnected event didn't fire in this scenario. It started working again in 1.0.0, it broke in 1.0.1 and is working again in 1.0.2.

johnkwaters avatar Oct 20 '18 17:10 johnkwaters

It should have never worked in any version. Maybe we just need to go back to the drawing board here and write a minimal example that reproduces the problem. A brand new project with authentication that uses the service and overrides both OnConnectedAsync and OnDisconnectedAsync in the hub. Put that sample on github with the expected behaviors and we can go from there.

davidfowl avatar Oct 20 '18 18:10 davidfowl

The part that was never reproduced in the Samples is that my Angular client is a separate SPA, not served up from the same domain as the SignalR Web API. I am not sure what broke and unbroke a few times, but during all the versions, my code was constant. Also, with regular SignalR always worked, it was just the Azure SignalR that didn't result in that event.

johnkwaters avatar Oct 20 '18 18:10 johnkwaters

So to be clear, you cannot reproduce the behavior with a simpler sample that uses the Azure SignalR SDK?

davidfowl avatar Oct 20 '18 18:10 davidfowl