I'm using SignalR in a React frontend with JWT tokens passed via HttpOnly cookies for authentication, which prevents access to the token in JavaScript.
When the server is down for too long, the JWT expire and need to be refreshed.
I have a working refresh-Token endpoint (using httpOnly too).
this.connection = new signalR.HubConnectionBuilder()
.withUrl(serverUrl)
.withAutomaticReconnect()
.build();
await this.connection.start();
this.connection.onclose(() => {
console.warn("SignalR Disconnected");
});
this.connection.onreconnecting((error) => {
console.warn("SignalR Reconnecting...", error);
});
this.connection.onreconnected((connectionId) => {
console.info(`SignalR Reconnected. Connection ID: ${connectionId}`);
});
JWTs are passed exclusively via HttpOnly cookies, so accessTokenFactory
cannot be used as per Microsoft's documentation.
A similar discussion exists here, but it focuses on accessTokenFactory
, which isn't applicable with HttpOnly cookies. There is one solution exposed here And I am wondering if there nothing else that could be done 3 years later.
Please avoid any "check if the message contains '401' or 'unauthorized'".
Is there a standard or recommended approach to handle JWT refresh with SignalR when using HttpOnly cookies, without fully re-implementing SignalR connection management logic?
Thank you.
I ended up with this approach, which is far from optimal but "kind of works":
stopConnectionCalled
boolean is set to avoid unnecessary token refresh attempts during nominal disconnections.// The `stopConnectionCalled` field prevents refresh attempts during intentional disconnections (e.g., logout).
private stopConnectionCalled: boolean = false;
async initConnection() {
// Reset `stopConnectionCalled` whenever initializing the connection.
this.stopConnectionCalled = false;
this.connection = new signalR.HubConnectionBuilder()
.withUrl(serverUrl)
.withAutomaticReconnect()
.build();
await this.connection.start();
this.connection.onclose(async () => {
// Attempt to recover the connection only if it wasn't manually stopped.
if (!this.stopConnectionCalled) {
const refreshedSuccessfully = await this.tryIfRefreshTokenNeeded(id);
if (refreshedSuccessfully) {
// Restart the SignalR connection manually after token refresh.
await this.connection?.start();
}
}
});
}
async stopConnection() {
// Mark as explicitly stopped to prevent unnecessary recovery attempts.
this.stopConnectionCalled = true;
// ...nominal stop connection logic
}
This function checks if a token refresh is needed by pinging an authenticated endpoint and attempts to refresh the token if required:
private async tryIfRefreshTokenNeeded(id: string): Promise<boolean> {
try {
// Ping the server with an authenticated request.
await api.server.pingAuthenticatedNoAutoRefresh();
} catch (error) {
// Handle 401 Unauthorized responses by refreshing the token.
if (error instanceof Response && error.status === 401) {
// 401 Unauthorized detected on ping, attempting token refresh
try {
await api.account.refreshToken();
return true; // Refresh succeeded
} catch {
return false; // failed to refresh the token
}
}
return false; // Any other error
}
return true; // No refresh needed
}
While this solution is a starting point, I am still open to a more robust approach.