fix: not refreshing token
This commit is contained in:
@@ -20,6 +20,8 @@ class TokenRefreshManager {
|
|||||||
private isRefreshing = false;
|
private isRefreshing = false;
|
||||||
private isStarted = false;
|
private isStarted = false;
|
||||||
private visibilityChangeHandler: (() => void) | null = null;
|
private visibilityChangeHandler: (() => void) | null = null;
|
||||||
|
private onlineHandler: (() => void) | null = null;
|
||||||
|
private focusHandler: (() => void) | null = null;
|
||||||
private lastRefreshTime: number | null = null;
|
private lastRefreshTime: number | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,6 +64,20 @@ class TokenRefreshManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
||||||
|
|
||||||
|
// Re-check on network reconnection (device was offline)
|
||||||
|
this.onlineHandler = () => {
|
||||||
|
console.log("[Token Refresh] Network reconnected, checking token status");
|
||||||
|
this.checkAndRefreshIfNeeded();
|
||||||
|
};
|
||||||
|
window.addEventListener("online", this.onlineHandler);
|
||||||
|
|
||||||
|
// Re-check on window focus (device was asleep or user switched apps)
|
||||||
|
this.focusHandler = () => {
|
||||||
|
console.log("[Token Refresh] Window focused, checking token status");
|
||||||
|
this.checkAndRefreshIfNeeded();
|
||||||
|
};
|
||||||
|
window.addEventListener("focus", this.focusHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,6 +97,16 @@ class TokenRefreshManager {
|
|||||||
this.visibilityChangeHandler = null;
|
this.visibilityChangeHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.onlineHandler) {
|
||||||
|
window.removeEventListener("online", this.onlineHandler);
|
||||||
|
this.onlineHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.focusHandler) {
|
||||||
|
window.removeEventListener("focus", this.focusHandler);
|
||||||
|
this.focusHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.isStarted = false;
|
this.isStarted = false;
|
||||||
this.lastRefreshTime = null; // Reset refresh time on stop
|
this.lastRefreshTime = null; // Reset refresh time on stop
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -530,6 +530,25 @@ async function restoreSessionFromDB(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this session has already been rotated (has a child session)
|
||||||
|
// If so, we cannot restore from it - user must create a new session
|
||||||
|
const childCheck = await conn.execute({
|
||||||
|
sql: `SELECT id FROM Session WHERE parent_session_id = ? LIMIT 1`,
|
||||||
|
args: [sessionId]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (childCheck.rows.length > 0) {
|
||||||
|
console.log(
|
||||||
|
"[Session Restore] Session has already been rotated - cannot restore"
|
||||||
|
);
|
||||||
|
// Revoke this session to ensure it's marked as used
|
||||||
|
await conn.execute({
|
||||||
|
sql: "UPDATE Session SET revoked = 1 WHERE id = ?",
|
||||||
|
args: [sessionId]
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// We can't restore the refresh token (it's hashed in DB)
|
// We can't restore the refresh token (it's hashed in DB)
|
||||||
// So we need to generate a new one and rotate the session
|
// So we need to generate a new one and rotate the session
|
||||||
|
|
||||||
@@ -551,7 +570,15 @@ async function restoreSessionFromDB(
|
|||||||
dbSession.token_family as string // Reuse family
|
dbSession.token_family as string // Reuse family
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("[Session Restore] Successfully created new session");
|
// Mark parent session as revoked now that we've rotated it
|
||||||
|
await conn.execute({
|
||||||
|
sql: "UPDATE Session SET revoked = 1 WHERE id = ?",
|
||||||
|
args: [sessionId]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[Session Restore] Successfully created new session and revoked parent"
|
||||||
|
);
|
||||||
return newSession;
|
return newSession;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Session Restore] Error restoring session:", error);
|
console.error("[Session Restore] Error restoring session:", error);
|
||||||
@@ -562,6 +589,7 @@ async function restoreSessionFromDB(
|
|||||||
/**
|
/**
|
||||||
* Validate session against database
|
* Validate session against database
|
||||||
* Checks if session exists, not revoked, not expired, and refresh token matches
|
* Checks if session exists, not revoked, not expired, and refresh token matches
|
||||||
|
* Also validates that no child sessions exist (indicating this session was rotated)
|
||||||
* @param sessionId - Session ID
|
* @param sessionId - Session ID
|
||||||
* @param userId - User ID
|
* @param userId - User ID
|
||||||
* @param refreshToken - Plaintext refresh token
|
* @param refreshToken - Plaintext refresh token
|
||||||
@@ -577,7 +605,7 @@ async function validateSessionInDB(
|
|||||||
const tokenHash = hashRefreshToken(refreshToken);
|
const tokenHash = hashRefreshToken(refreshToken);
|
||||||
|
|
||||||
const result = await conn.execute({
|
const result = await conn.execute({
|
||||||
sql: `SELECT revoked, expires_at, refresh_token_hash
|
sql: `SELECT revoked, expires_at, refresh_token_hash, token_family
|
||||||
FROM Session
|
FROM Session
|
||||||
WHERE id = ? AND user_id = ?`,
|
WHERE id = ? AND user_id = ?`,
|
||||||
args: [sessionId, userId]
|
args: [sessionId, userId]
|
||||||
@@ -611,6 +639,41 @@ async function validateSessionInDB(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRITICAL: Check if this session has been rotated (has a child session)
|
||||||
|
// If a child exists, check if we're within the grace period for cookie propagation
|
||||||
|
// This handles SSR/serverless cases where client may not have received new cookies yet
|
||||||
|
const childCheck = await conn.execute({
|
||||||
|
sql: `SELECT id, created_at FROM Session
|
||||||
|
WHERE parent_session_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1`,
|
||||||
|
args: [sessionId]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (childCheck.rows.length > 0) {
|
||||||
|
// This session has been rotated - check grace period
|
||||||
|
const childSession = childCheck.rows[0];
|
||||||
|
const childCreatedAt = new Date(childSession.created_at as string);
|
||||||
|
const now = new Date();
|
||||||
|
const timeSinceRotation = now.getTime() - childCreatedAt.getTime();
|
||||||
|
|
||||||
|
// Grace period allows client to receive and use new cookies from rotation
|
||||||
|
// This is critical for SSR/serverless where response cookies may be delayed
|
||||||
|
if (timeSinceRotation >= AUTH_CONFIG.REFRESH_TOKEN_REUSE_WINDOW_MS) {
|
||||||
|
// Grace period expired - parent session should no longer be used
|
||||||
|
// This indicates either token theft or client failed to update cookies
|
||||||
|
console.log(
|
||||||
|
`[Session Validation] Parent session used ${Math.round(timeSinceRotation / 1000)}s after rotation (grace period expired)`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Within grace period - allow parent session use while cookies propagate
|
||||||
|
console.log(
|
||||||
|
`[Session Validation] Parent session used ${Math.round(timeSinceRotation / 1000)}s after rotation (within grace period)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update last_used and last_active_at timestamps (throttled)
|
// Update last_used and last_active_at timestamps (throttled)
|
||||||
// Only update DB if last update was > 5 minutes ago (reduces writes by 95%+)
|
// Only update DB if last update was > 5 minutes ago (reduces writes by 95%+)
|
||||||
updateSessionActivityThrottled(sessionId).catch((err) =>
|
updateSessionActivityThrottled(sessionId).catch((err) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user