I’m working on a web application that uses SignalR with WebSockets for real-time communication. When establishing a connection, I first issue a jwt token and then it is used to create the SignalR connection.
I’ve noticed that even when the token expires, my SignalR WebSocket connection continues to function if I don’t explicitly reconnect. However, I’m concerned about the correctness of this behavior.
My questions are:
Should I issue a new jwt token again to obtain a new token once the current one has expired, even if the WebSocket connection is still active?
If the token expires but the WebSocket connection is still working, is it safe to continue using the same connection, or should I always reissue the jwt token to ensure its valid?
What is the best practice for handling expired tokens in a long-running SignalR WebSocket connection?
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { negotiateSignalR } from '/services';
import { extractErrorMessage } from '/utils';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
interface SignalRContextType {
connection: HubConnection;
}
const SignalRContext = createContext<SignalRContextType | undefined>(undefined);
export const SignalRProvider = ({ children }: { children: ReactNode }) => {
const [connection, setConnection] = useState<HubConnection | null>(null);
const createConnection = useCallback(
async (url: string, accessToken: string) => {
const completeUrl = `${url}/${path}`;
const newConnection = new HubConnectionBuilder()
.withUrl(completeUrl, {
accessTokenFactory: async () => accessToken,
})
.withAutomaticReconnect()
.build();
newConnection.onreconnecting((err) => {
//TODO: token expires after 1 hour, add logic here to check if it has expired, if it is fetch a new one
console.log('err reconnecting', err);
});
try {
await newConnection.start();
setConnection(newConnection);
} catch (error) {
toast.error(extractErrorMessage(error));
}
},
[]
);
const setupConnection = useCallback(async () => {
try {
const response = await negotiateSignalR();
const { url, accessToken } = response.data;
if (url && accessToken) {
await createConnection(url, accessToken);
}
} catch (error) {
toast.error(extractErrorMessage(error));
}
}, [createConnection]);
useEffect(() => {
setupConnection();
}, [setupConnection]);
return (
<SignalRContext.Provider value={{ connection }}>
{children}
</SignalRContext.Provider>
);
};