updating auth token security
This commit is contained in:
@@ -18,6 +18,47 @@ export async function safeFetch(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode JWT payload without verification (client-side only)
|
||||
* @param token - JWT token string
|
||||
* @returns Decoded payload or null if invalid
|
||||
*/
|
||||
export function decodeJWT(token: string): {
|
||||
id: string;
|
||||
sid: string;
|
||||
exp: number;
|
||||
iat: number;
|
||||
} | null {
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const payload = JSON.parse(
|
||||
atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"))
|
||||
);
|
||||
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time until JWT expires (in milliseconds)
|
||||
* @param token - JWT token string
|
||||
* @returns Milliseconds until expiry, or null if invalid/expired
|
||||
*/
|
||||
export function getTimeUntilExpiry(token: string): number | null {
|
||||
const payload = decodeJWT(token);
|
||||
if (!payload || !payload.exp) return null;
|
||||
|
||||
const expiryMs = payload.exp * 1000;
|
||||
const now = Date.now();
|
||||
const timeUntil = expiryMs - now;
|
||||
|
||||
return timeUntil > 0 ? timeUntil : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts soft hyphens (­) for manual hyphenation. Uses actual characters for Typewriter compatibility.
|
||||
*/
|
||||
|
||||
153
src/lib/token-refresh.ts
Normal file
153
src/lib/token-refresh.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Token Refresh Manager
|
||||
* Handles automatic token refresh before expiry
|
||||
*/
|
||||
|
||||
import { api } from "~/lib/api";
|
||||
import { getClientCookie } from "~/lib/cookies.client";
|
||||
import { getTimeUntilExpiry } from "~/lib/client-utils";
|
||||
|
||||
class TokenRefreshManager {
|
||||
private refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private isRefreshing = false;
|
||||
private refreshThresholdMs = 2 * 60 * 1000; // Refresh 2 minutes before expiry
|
||||
private isStarted = false;
|
||||
private visibilityChangeHandler: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Start monitoring token and auto-refresh before expiry
|
||||
*/
|
||||
start(): void {
|
||||
if (typeof window === "undefined") return; // Server-side bail
|
||||
if (this.isStarted) return; // Already started, prevent duplicate listeners
|
||||
|
||||
this.isStarted = true;
|
||||
this.scheduleNextRefresh();
|
||||
|
||||
// Re-check on visibility change (user returns to tab)
|
||||
this.visibilityChangeHandler = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
this.scheduleNextRefresh();
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring and clear timers
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
|
||||
if (this.visibilityChangeHandler) {
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this.visibilityChangeHandler
|
||||
);
|
||||
this.visibilityChangeHandler = null;
|
||||
}
|
||||
|
||||
this.isStarted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule next refresh based on token expiry
|
||||
*/
|
||||
private scheduleNextRefresh(): void {
|
||||
this.stop(); // Clear existing timer
|
||||
|
||||
const token = getClientCookie("userIDToken");
|
||||
if (!token) {
|
||||
// No token found - user not logged in, nothing to refresh
|
||||
return;
|
||||
}
|
||||
|
||||
const timeUntilExpiry = getTimeUntilExpiry(token);
|
||||
if (!timeUntilExpiry) {
|
||||
console.warn("Token expired or invalid, attempting refresh now");
|
||||
this.refreshNow();
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule refresh before expiry
|
||||
const timeUntilRefresh = Math.max(
|
||||
0,
|
||||
timeUntilExpiry - this.refreshThresholdMs
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[Token Refresh] Token expires in ${Math.round(timeUntilExpiry / 1000)}s, ` +
|
||||
`scheduling refresh in ${Math.round(timeUntilRefresh / 1000)}s`
|
||||
);
|
||||
|
||||
this.refreshTimer = setTimeout(() => {
|
||||
this.refreshNow();
|
||||
}, timeUntilRefresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform token refresh immediately
|
||||
*/
|
||||
async refreshNow(): Promise<boolean> {
|
||||
if (this.isRefreshing) {
|
||||
console.log("[Token Refresh] Refresh already in progress, skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
console.log("[Token Refresh] Refreshing access token...");
|
||||
|
||||
const result = await api.auth.refreshToken.mutate({
|
||||
rememberMe: false // Maintain existing rememberMe state
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log("[Token Refresh] Token refreshed successfully");
|
||||
this.scheduleNextRefresh(); // Schedule next refresh
|
||||
return true;
|
||||
} else {
|
||||
console.error("[Token Refresh] Token refresh failed:", result);
|
||||
this.handleRefreshFailure();
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Token Refresh] Token refresh error:", error);
|
||||
this.handleRefreshFailure();
|
||||
return false;
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle refresh failure (redirect to login)
|
||||
*/
|
||||
private handleRefreshFailure(): void {
|
||||
console.warn("[Token Refresh] Token refresh failed, redirecting to login");
|
||||
|
||||
// Store current URL for redirect after login
|
||||
const currentPath = window.location.pathname + window.location.search;
|
||||
if (currentPath !== "/login") {
|
||||
sessionStorage.setItem("redirectAfterLogin", currentPath);
|
||||
}
|
||||
|
||||
// Redirect to login
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const tokenRefreshManager = new TokenRefreshManager();
|
||||
|
||||
/**
|
||||
* Manually trigger token refresh (can be called from UI)
|
||||
* @returns Promise<boolean> success status
|
||||
*/
|
||||
export async function manualRefresh(): Promise<boolean> {
|
||||
return tokenRefreshManager.refreshNow();
|
||||
}
|
||||
Reference in New Issue
Block a user