6

I create a realtime connection via SignalR From client(angular 9) and server(asp.net core 3.1) and Authorize hub by JWT Token such as below code :

 private createConnection() {
      this.hubConnection = new HubConnectionBuilder().withUrl(`${this.appConfig.hubEndpoint}/Hubs`,
        { accessTokenFactory: () => jwtToken })
        .withAutomaticReconnect()
        .build();
  }

  private startConnection(): void {
    this.hubConnection
      .start()
      .then(() => {
        this.connectionIsEstablished = true;
        this.connectionEstablished.emit(true);
      })
      .catch(err => {
        console.log('Error while establishing connection, retrying...');
      });
  }

this works fine until the token expired. According to my research, after receiving the new token with the refresh token, the previous connection should be stopped and a new connection should be created with the new token. Now I want to know how should I do this? Do I have to constantly check the token? Or should this be addressed by sending each request to the server?

mahdi rahimzade
  • 111
  • 1
  • 8
  • did you find a way? – Kardon63 Mar 04 '21 at 12:12
  • @Kardon63 did you find a way too ? xD – Mehdi Benmoha May 06 '21 at 10:16
  • @MehdiBenmoha On the close event you can check if the error string contains `unauthorized` and then you can restart the connection, didn't implement it yet but this is how I think it should be done – Kardon63 May 06 '21 at 10:31
  • 1
    I am giving it a try.. will update you if I get it working – Mehdi Benmoha May 06 '21 at 10:48
  • @Kardon63 the onclose event is triggered but the string doesn't contain unauthorized, the listener gets an `undefined` value. It was also very hard for me to renew the connection because I am using an async call to get the token and I am wrapping the whole system with rxJS, so the quick and dirty fix was to reload the page when a disconnected event is received, while also setting aggressive delays for automatic reconnects. – Mehdi Benmoha May 25 '21 at 09:28

6 Answers6

6

The solution I came up with is to intercept the auth calls of the signalR client by extending the signalR.DefaultHttpClient it uses. If there is a 401 then I refresh the token (via my authService), and retry the call:

Typescript:

const getAuthHeaders = () => {
  return {
    Authorization: `Bearer ${authService.getToken()?.accessToken}`,
  };
};

class CustomHttpClient extends signalR.DefaultHttpClient {
  constructor() {
    super(console); // the base class wants a signalR.ILogger
  }
  public async send(
    request: signalR.HttpRequest
  ): Promise<signalR.HttpResponse> {
    const authHeaders = getAuthHeaders();
    request.headers = { ...request.headers, ...authHeaders };

    try {
      const response = await super.send(request);
      return response;
    } catch (er) {
      if (er instanceof signalR.HttpError) {
        const error = er as signalR.HttpError;
        if (error.statusCode == 401) {
          //token expired - trying a refresh via refresh token
          await authService.refresh();
          const authHeaders = getAuthHeaders();
          request.headers = { ...request.headers, ...authHeaders };
        }
      } else {
        throw er;
      }
    }
    //re try the request
    return super.send(request);
  }
}

const connection = new signalR.HubConnectionBuilder()
  .withUrl("/MyHub", {
// use the custom client
    httpClient: new CustomHttpClient(),
  })
  .configureLogging(signalR.LogLevel.Information)
  .build();

see the options of .withUrl(..) here: https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration?view=aspnetcore-6.0&tabs=dotnet

  • This solution helped when I used a self hosted SignalR Service. Since I am using the Azure SignalR Service, the negotiation always fails and I don't receive any messages from my hubs anymore. @TwoFingerRightClick any Idea what's going on here? – Weissvonnix Jul 10 '22 at 19:56
  • This HTTP-Client implementation does not work with Azure SignalR Service, see this for details: https://stackoverflow.com/a/72993339/2588938 – Weissvonnix Jul 15 '22 at 11:34
0

When the token will expire the connection will be droped by the server and you will have the error on the server side. I believe is the 405 error code Method now allowed that you will get.

So what you need is catch this token expiration error and drop the connection so you can start a new one with the new token.

Kiril1512
  • 3,231
  • 3
  • 16
  • 41
  • 1
    I do not receive an error when the token expires. The error occurs when a method of the hub is executed. Can you introduce an article or an example implemented in this field? – mahdi rahimzade Jun 03 '20 at 17:55
  • 1
    You have the token expiration time, imagine you define 1 hour for your token, then define on your hub the connection to be valid only for 1 our using the `ConnectionTimeout`. – Kiril1512 Jun 04 '20 at 08:53
  • @Kiril1512 can i refresh the token in the on reconnect event, or i need to force a new creation for the connection, can you give an example please? – Kardon63 Mar 05 '21 at 09:35
  • @Kardon63 sorry, I don't know if you can do this on reconnect – Kiril1512 Mar 05 '21 at 14:39
0

According to microsoft documentation, the renew of the token have to be done within the accessToken function.

"The access token function provided is called before every HTTP request made by SignalR. If the token needs to be renewed in order to keep the connection active, do so from within this function and return the updated token. The token may need to be renewed so it doesn't expire during the connection."

In case of the link above, it needs to be done in "this.loginToken."

Andlab
  • 43
  • 7
0

I had same problem and I solved in this way. I'm using Angular 14 and NET Core 6.

First I create a Subject<string> in the service I use for renew the access token where I emit the new access token when I renew it (or an empty string when I logout).

tokenRefreshed$: Subject<string> = new Subject();
public PersistTokens(accessToken?: string, refreshToken?: string) {
  this.tokenRefreshed$.next(accessToken??'');
  SetJWTAccessToken(accessToken??'');
  SetJWTRefreshToken(refreshToken??'');
}

After, in the service where I start signalr hub connection, I subscribe to the Subject described before... then I use the new access token in the connection request.

constructor(private appService: ApplicationService, private jwtTokenService:JwtTokenService) {
  // first hub connection on application start or page refresh (I want to allow anonymous siglr connection too)
  this.startConnection(GetJWTAccessToken()??'');
 
  // if user logged in restart the hub connection to handle the new token
  jwtTokenService.tokenRefreshed$.subscribe(
    token => {
      // if already connected stop existing connection
      if(this.hubConnection && this.hubConnection.state === signalR.HubConnectionState.Connected) {
        this.hubConnection.stop();
      }
      this.startConnection(token);
    }
  );
}

async startConnection(accesToken:string) {
  this.hubConnection = new signalR.HubConnectionBuilder()      
    .withUrl(`${this.appService.appConfiguration.apiEndpointUrl}/hubs/notifyHub`, {
      skipNegotiation: true,
      transport: signalR.HttpTransportType.WebSockets,
      accessTokenFactory: () => {
        return accesToken;
    }
  })
  .build();

this.hubConnection
  .start()
  .then(() => console.debug('Notify hub connected'))
  // .then(() => this.getConnectionId())
  .catch(err => console.log('Error while starting Notify hub connection: ' + err));

this.hubConnection.on('NotifyOrderCreated', (data) => {
  this.notifyOrderCreated$.emit(data);
});
// Other messages subscriptions ...

}

The function GetJWTAccessToken() simply read the access token saved in the browser storage.

Fabio Cavallari
  • 124
  • 3
  • 10
0

There is no "good" way of doing that, after 5 years they came up with:

public class HttpConnectionDispatcherOptions
{
     bool EnableAuthenticationExpiration { get; set; }
} 

It will force reconnect when token expires, but it is resource heavy solution.

See https://github.com/dotnet/aspnetcore/issues/5297 for more info...

kemsky
  • 14,727
  • 3
  • 32
  • 51
-2

What did work for me, it's a QUICK AND DIRTY fix, was to reload the page on close events:

this.hubConnection.onclose(() =>{
  window.location.reload()
})

Explanation

I am wrapping the connection process with RxJS, so a better fix for my case is to throw an error instead of reloading the page and catch it with retryWhen operator. But as this is a hard bug (need to wait 1h for the token to expire, and locally the emulator doesnt care about tokens...), I just prefered to go with this temporary solution.

Mehdi Benmoha
  • 3,694
  • 3
  • 23
  • 43