From 210e562be50130db42fde9bfe49e91e7f4eb8d9c Mon Sep 17 00:00:00 2001 From: anshul23102 Date: Wed, 3 Jun 2026 18:40:44 +0530 Subject: [PATCH] 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 --- src/hooks/useGitHubAuth.ts | 57 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/src/hooks/useGitHubAuth.ts b/src/hooks/useGitHubAuth.ts index a0c24b2a..23a9cbf7 100644 --- a/src/hooks/useGitHubAuth.ts +++ b/src/hooks/useGitHubAuth.ts @@ -1,14 +1,64 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { Octokit } from '@octokit/core'; +// Inactivity timeout in milliseconds (30 minutes). +// If the user has not interacted with the application for this period, +// the session credentials are cleared automatically. This limits the +// window during which a stolen or leaked token remains usable. +const SESSION_TIMEOUT_MS = 30 * 60 * 1000; + export const useGitHubAuth = () => { const [username, setUsername] = useState(''); const [token, setToken] = useState(''); + const timeoutRef = useRef | null>(null); + + // Clear credentials on session expiry. + const clearSession = useCallback(() => { + setUsername(''); + setToken(''); + }, []); + + // Reset the inactivity timer on every interaction. + // The timer is only active when a username is set (i.e., a session exists). + const resetTimer = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + if (username) { + timeoutRef.current = setTimeout(clearSession, SESSION_TIMEOUT_MS); + } + }, [username, clearSession]); + + useEffect(() => { + if (!username) { + // No active session; clear any lingering timer. + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + return; + } + + const events = ['mousemove', 'keydown', 'click', 'scroll', 'touchstart']; + + // Start the timer immediately when a session begins. + resetTimer(); + + events.forEach((e) => window.addEventListener(e, resetTimer)); + + return () => { + events.forEach((e) => window.removeEventListener(e, resetTimer)); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [username, resetTimer]); + const octokit = useMemo(() => { if (!username) return null; - if(token){ - return new Octokit({ auth: token }); + if (token) { + return new Octokit({ auth: token }); } return new Octokit(); }, [username, token]); @@ -20,6 +70,7 @@ export const useGitHubAuth = () => { setUsername, token, setToken, + clearSession, getOctokit, }; };