Skip to content

Commit 210e562

Browse files
committed
fix(auth): auto-clear session credentials after 30 minutes of inactivity
Sessions had no expiration: once a username and optional PAT were entered they remained in React state indefinitely. An unattended browser tab with a valid PAT stored in state remained exploitable for the entire browser session. Add an inactivity timer via useEffect. After 30 minutes without a user interaction event (mousemove, keydown, click, scroll, touchstart) the timer fires clearSession(), zeroing both username and token. The timer resets on every qualifying event so active sessions are not disrupted. When no username is set the timer is inactive and no listeners are registered. Closes #688
1 parent 53f820b commit 210e562

1 file changed

Lines changed: 54 additions & 3 deletions

File tree

src/hooks/useGitHubAuth.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,64 @@
1-
import { useState, useMemo } from 'react';
1+
import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
22
import { Octokit } from '@octokit/core';
33

4+
// Inactivity timeout in milliseconds (30 minutes).
5+
// If the user has not interacted with the application for this period,
6+
// the session credentials are cleared automatically. This limits the
7+
// window during which a stolen or leaked token remains usable.
8+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
9+
410
export const useGitHubAuth = () => {
511
const [username, setUsername] = useState('');
612
const [token, setToken] = useState('');
713

14+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
15+
16+
// Clear credentials on session expiry.
17+
const clearSession = useCallback(() => {
18+
setUsername('');
19+
setToken('');
20+
}, []);
21+
22+
// Reset the inactivity timer on every interaction.
23+
// The timer is only active when a username is set (i.e., a session exists).
24+
const resetTimer = useCallback(() => {
25+
if (timeoutRef.current) {
26+
clearTimeout(timeoutRef.current);
27+
}
28+
if (username) {
29+
timeoutRef.current = setTimeout(clearSession, SESSION_TIMEOUT_MS);
30+
}
31+
}, [username, clearSession]);
32+
33+
useEffect(() => {
34+
if (!username) {
35+
// No active session; clear any lingering timer.
36+
if (timeoutRef.current) {
37+
clearTimeout(timeoutRef.current);
38+
timeoutRef.current = null;
39+
}
40+
return;
41+
}
42+
43+
const events = ['mousemove', 'keydown', 'click', 'scroll', 'touchstart'];
44+
45+
// Start the timer immediately when a session begins.
46+
resetTimer();
47+
48+
events.forEach((e) => window.addEventListener(e, resetTimer));
49+
50+
return () => {
51+
events.forEach((e) => window.removeEventListener(e, resetTimer));
52+
if (timeoutRef.current) {
53+
clearTimeout(timeoutRef.current);
54+
}
55+
};
56+
}, [username, resetTimer]);
57+
858
const octokit = useMemo(() => {
959
if (!username) return null;
10-
if(token){
11-
return new Octokit({ auth: token });
60+
if (token) {
61+
return new Octokit({ auth: token });
1262
}
1363
return new Octokit();
1464
}, [username, token]);
@@ -20,6 +70,7 @@ export const useGitHubAuth = () => {
2070
setUsername,
2171
token,
2272
setToken,
73+
clearSession,
2374
getOctokit,
2475
};
2576
};

0 commit comments

Comments
 (0)