Skip to content

Commit 04eb2f4

Browse files
committed
fix(Client): add time synchronization to fix authentication time window issues
1 parent 1a6775e commit 04eb2f4

4 files changed

Lines changed: 205 additions & 560 deletions

File tree

backend-servers/server/commandHandler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export class CommandHandler {
114114
message: "Client registered with success!",
115115
client_id: username,
116116
status: "registered",
117+
timestamp: Date.now(),
117118
ttl_info: {
118119
client_ttl_hours: CONFIG.CLIENT_TTL / 3600,
119120
message_ttl_hours: CONFIG.MESSAGE_TTL / 3600
@@ -587,6 +588,7 @@ export class CommandHandler {
587588
};
588589
break;
589590
}
591+
590592
case command === "get_messages": {
591593
const room_id = currentClient.room_id;
592594
if (room_id) {
@@ -667,7 +669,8 @@ export class CommandHandler {
667669
response = {
668670
...response,
669671
message: "Heartbeat received",
670-
client_status: "alive"
672+
client_status: "alive",
673+
timestamp: Date.now()
671674
};
672675
break;
673676
}

client/src/shared/authContext.tsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ import UsernameModal from '../components/UsernameModal';
77

88
const ClientContext = createContext<ClientContextType | null>(null);
99

10+
let timeOffset = 0;
11+
let timeSynced = false;
12+
13+
export const getAdjustedTimestamp = (): string => {
14+
return new Date(Date.now() + timeOffset).toISOString();
15+
};
16+
17+
const calculateTimeOffset = (serverTimestamp: number): number => {
18+
const clientTime = Date.now();
19+
const offset = serverTimestamp - clientTime;
20+
console.log(`Server time: ${new Date(serverTimestamp).toISOString()}`);
21+
console.log(`Client time: ${new Date(clientTime).toISOString()}`);
22+
console.log(`Calculated offset: ${offset}ms`);
23+
return offset;
24+
};
25+
26+
27+
1028
const validateUsername = (username: string): boolean => {
1129
const regex = /^[a-zA-Z0-9_-]{3,16}$/;
1230
return regex.test(username);
@@ -18,6 +36,7 @@ export const ClientProvider = ({ children }: { children: ComponentChildren }) =>
1836
const [loading, setLoading] = useState(true);
1937
const [showUsernameModal, setShowUsernameModal] = useState(false);
2038
const [usernamePromptReason, setUsernamePromptReason] = useState<string>('');
39+
const [isTimeSynced, setIsTimeSynced] = useState(false);
2140

2241
const promptForUsername = (reason: string = 'Enter a username') => {
2342
setUsernamePromptReason(reason);
@@ -39,6 +58,15 @@ export const ClientProvider = ({ children }: { children: ComponentChildren }) =>
3958
}
4059
};
4160

61+
62+
useEffect(() => {
63+
if(status !== "open") return;
64+
65+
66+
setIsTimeSynced(false);
67+
timeSynced = false;
68+
}, [status]);
69+
4270
useEffect(() => {
4371
if(status !== "open") return;
4472

@@ -58,26 +86,42 @@ export const ClientProvider = ({ children }: { children: ComponentChildren }) =>
5886
}
5987

6088
setUsername(savedUsername);
61-
sendAuthenticatedMessage(sendMessage, { command: "heartbeat", client_id: savedUsername });
89+
90+
91+
sendMessage({ command: "heartbeat", client_id: savedUsername });
6292
})();
6393
}, [status, sendMessage]);
6494

95+
6596
useEffect(() => {
6697
if(!messages.length) return;
6798

6899
const lastMessage = messages[messages.length - 1];
69100

101+
102+
if((lastMessage.command === "upload_public_key" || lastMessage.command === "heartbeat") && lastMessage.timestamp) {
103+
const newOffset = calculateTimeOffset(lastMessage.timestamp);
104+
timeOffset = newOffset;
105+
timeSynced = true;
106+
setIsTimeSynced(true);
107+
console.log('Time synchronized with server via', lastMessage.command);
108+
}
109+
110+
70111
if(typeof lastMessage.command === 'string' && lastMessage.command === "upload_public_key" && lastMessage.client_id){
71112
localStorage.setItem('username', lastMessage.client_id);
72113
setUsername(lastMessage.client_id);
73114
setLoading(false);
74115
return;
75116
}
76117

118+
77119
if(lastMessage.command === 'heartbeat'){
78120
if(lastMessage.error){
79121
localStorage.removeItem('username');
80122
setUsername(null);
123+
setIsTimeSynced(false);
124+
timeSynced = false;
81125
promptForUsername('Session expired. Please re-enter your username.');
82126
}
83127
else{
@@ -88,8 +132,9 @@ export const ClientProvider = ({ children }: { children: ComponentChildren }) =>
88132
}
89133
}, [messages]);
90134

135+
91136
useEffect(() => {
92-
if(!username || status !== 'open') return;
137+
if(!username || status !== 'open' || !isTimeSynced) return;
93138

94139
const heartbeatInterval = setInterval(async () => {
95140
try{
@@ -104,10 +149,13 @@ export const ClientProvider = ({ children }: { children: ComponentChildren }) =>
104149
}, 25000);
105150

106151
return () => clearInterval(heartbeatInterval);
107-
}, [username, status, sendMessage]);
152+
}, [username, status, sendMessage, isTimeSynced]);
108153

109154
return (
110-
<ClientContext.Provider value={{ username, loading }}>
155+
<ClientContext.Provider value={{
156+
username,
157+
loading: loading || !isTimeSynced
158+
}}>
111159
{children}
112160
<UsernameModal
113161
isOpen={showUsernameModal}

0 commit comments

Comments
 (0)