Home > Net >  SignalR connection falls back to longpolling because of 401
SignalR connection falls back to longpolling because of 401

Time:11-10

I have a .net 6 backend with an Angular 13 backend, that uses JWT tokens for auth. For some reason SignalR always falls back to longpolling, both on prod and on dev machine, it seems that it's call to negotiate?negotiateVersion=1 is successful, and it chooses WebSockets, but afterwards it's call to localhost:PORT/hubs/myhub?id=[ID]&access_token=[JWTTOKEN] is returned with a 401.

The Angular part is using NGRX to get the JWT token, and the JWT token expires after 5 minutes. When it receives a 401 after connection is established it disconnects, makes a normal renew call, and connects again with the new JWT token. However the request described above will always return 401 even with a valid token.

My SignalR service:

export class NotificationSignalrService {

  private connection: signalR.HubConnection;
  connectionClosedRefreshTokenSubscription: Subscription | undefined;
  startConnectionRefreshTokenSubscription: Subscription | undefined;

  constructor(@Inject(APP_CONFIG) private appConfig: any, private store: Store) {
    this.connection = new signalR.HubConnectionBuilder()
      .withUrl(`${this.appConfig.SIGNALR}/hubs/notificationhub`, this.hubConnectionOptions)
      .configureLogging(signalR.LogLevel.Debug)
      //.withAutomaticReconnect()
      .build();

    this.connection.onclose(error => {
      console.log(`Forbindelse lukket pga: ${error}`);
      this.store.dispatch(AuthActions.renewNoLoading());
      this.connectionClosedRefreshTokenSubscription = this.store.select(AuthSelectors.selectTokenRefreshed).subscribe({
        next: tokenRefreshed => {
          if (tokenRefreshed) {
            this.connectionClosedRefreshTokenSubscription?.unsubscribe();

            this.startSignalRConnection();
          }
        }
      })
    });


    this.startSignalRConnection();
    this.startListening();
  }

  startSignalRConnection() {
    this.connection.start().catch(error => {
      console.log(`Der skete en fejl ved start af signalR ${error}`);
      this.startConnectionRefreshTokenSubscription = this.store.select(AuthSelectors.selectTokenRefreshed).subscribe({
        next: tokenRefreshed => {
          if (tokenRefreshed) {
            this.startConnectionRefreshTokenSubscription?.unsubscribe();
            this.connection.start().catch(error => console.log(`Kunne ikke starte forbindelsen efter renew ${error}`));
          }
        }
      })
    });

  }

  @HostListener('window:beforeunload', ['$event'])
  beforeunloadHandler() {
    this.connection.stop();
  }

  protected get hubConnectionOptions(): IHttpConnectionOptions {
    // NOTE: The auth token must be updated for each request. So using headers option is not true.
    //       Also for websockets and some other protocols signalr cannot set auth headers.
    //       See https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0#bearer-token-authentication

    return {
      /*headers,*/
      accessTokenFactory: () => {
        return this.store.select(AuthSelectors.getLoggedInToken)
          .pipe(take(1), filter(x => x !== null), map(x => x === null ? "" : x)).toPromise();

        // this.authService.refreshLogin()
        //   .pipe(map(_ => this.authService.accessToken)).toPromise();
      }
    };

    // NOTE:
    // The access token function you provide is called before every HTTP request made by SignalR. If you need to renew the token in order to keep the connection active (because it may expire during the connection), do so from within this function and return the updated token.
    // In standard web APIs, bearer tokens are sent in an HTTP header. However, SignalR is unable to set these headers in browsers when using some transports. When using WebSockets and Server - Sent Events, the token is transmitted as a query string parameter.
  }
  getAuthToken() {
    let token = '';
    this.store.select(AuthSelectors.getLoggedInToken).pipe(take(1))
      .subscribe(authToken => token = authToken ?? "");
    return {
      Authorization: `Bearer ${token}`
    };
  }

  startListening() {
    this.connection.on("NewNotificationForUser", (notification: NotificationsEntity) =>
      this.store.dispatch(NotificationsState.NotificationsActions.newNotification({ notification }))
    );
  }

in .net under Startup i have services.AddSignalR(); under ConfigureServices and

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
    endpoints.MapHub<NotificationHub>("/hubs/notificationhub");
});

in Configure

My Hub has a [Authorize] attribute.

CodePudding user response:

You probably aren't handling the access_token query string parameter. This is required when using WebSockets because the browser APIs for WebSockets do not support setting headers.

The docs explain how to handle the query string https://learn.microsoft.com/aspnet/core/signalr/authn-and-authz?view=aspnetcore-7.0#built-in-jwt-authentication

  • Related