fix(auth): invalidate session cache on login
Stale sessions from a stolen device could be reused up to the cache TTL after a legitimate user re-logs in, because login never cleared the existing cache entry. Changes: - Add invalidate_by_user(user_id) to SessionCache protocol - InMemorySessionCache maintains a user_id -> set[token] index to support O(1) invalidation of all sessions for a given user - NoOpSessionCache stub updated for API compatibility - auth_service.login() now returns the Session object alongside signed_token and expires_at - login router calls session_cache.invalidate_by_user(session.id) immediately after successful authentication Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -60,6 +60,7 @@ async def login(
|
||||
session_ctx: SessionServiceContextDep,
|
||||
settings: SettingsDep,
|
||||
rate_limiter: LoginRateLimiterDep,
|
||||
session_cache: SessionCacheDep,
|
||||
) -> LoginResponse:
|
||||
"""Verify the master password and return a session token.
|
||||
|
||||
@@ -71,6 +72,10 @@ async def login(
|
||||
Requests during the penalty period return ``429 Too Many Requests`` with
|
||||
a ``Retry-After`` header.
|
||||
|
||||
Cache invalidation: On successful login, any existing cached sessions for
|
||||
the same user are invalidated so that stale tokens (e.g., from a stolen
|
||||
device) cannot be reused beyond the cache TTL window.
|
||||
|
||||
Args:
|
||||
body: Login request validated by Pydantic.
|
||||
response: FastAPI response object used to set the cookie.
|
||||
@@ -78,6 +83,7 @@ async def login(
|
||||
session_ctx: Session service context containing db and repository.
|
||||
settings: Application settings (used for session duration and trusted proxies).
|
||||
rate_limiter: The login rate limiter (per IP).
|
||||
session_cache: Session cache for invalidating old sessions on login.
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.auth.LoginResponse` containing the token.
|
||||
@@ -94,7 +100,7 @@ async def login(
|
||||
raise RateLimitError("Too many login attempts. Please try again later.", retry_after_seconds=60.0)
|
||||
|
||||
try:
|
||||
signed_token, expires_at = await auth_service.login(
|
||||
signed_token, expires_at, session = await auth_service.login(
|
||||
session_ctx.db,
|
||||
password=body.password,
|
||||
session_duration_minutes=settings.session_duration_minutes,
|
||||
@@ -107,6 +113,10 @@ async def login(
|
||||
log.warning("login_failed", client_ip=client_ip, error=str(exc))
|
||||
raise AuthenticationError(str(exc)) from exc
|
||||
|
||||
# Invalidate any cached sessions for the same user to prevent reuse of
|
||||
# stale tokens (e.g., from a stolen device) beyond the cache TTL window.
|
||||
session_cache.invalidate_by_user(session.id)
|
||||
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
value=signed_token,
|
||||
|
||||
Reference in New Issue
Block a user