Store Session.validated rather than Session.expires. Already handled in db migration that drops old sessions.
This commit is contained in:
@@ -7,6 +7,7 @@ import os
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import paskia.db.operations as _ops
|
||||
from paskia.authsession import EXPIRES
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,7 +28,8 @@ def cleanup_expired() -> int:
|
||||
"""Remove expired sessions and reset tokens. Returns count removed."""
|
||||
now = datetime.now(UTC)
|
||||
count = 0
|
||||
expired_sessions = [k for k, s in _ops._db.sessions.items() if s.expiry < now]
|
||||
limit = now - EXPIRES
|
||||
expired_sessions = [k for k, s in _ops._db.sessions.items() if s.validated < limit]
|
||||
if expired_sessions:
|
||||
from paskia import oidc_notify # noqa: PLC0415
|
||||
|
||||
|
||||
@@ -447,7 +447,7 @@ def update_session(
|
||||
host: str | None = None,
|
||||
ip: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
expiry: datetime | None = None,
|
||||
validated: datetime | None = None,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
@@ -462,8 +462,8 @@ def update_session(
|
||||
s.ip = ip
|
||||
if user_agent is not None:
|
||||
s.user_agent = user_agent
|
||||
if expiry is not None:
|
||||
s.expiry = expiry
|
||||
if validated is not None:
|
||||
s.validated = validated
|
||||
|
||||
|
||||
def set_session_host(
|
||||
@@ -593,7 +593,7 @@ def login(
|
||||
host=host,
|
||||
ip=ip,
|
||||
user_agent=user_agent,
|
||||
expiry=now + duration,
|
||||
validated=now,
|
||||
)
|
||||
user_str = str(user_uuid)
|
||||
with _db.transaction("login", user=user_str):
|
||||
@@ -666,7 +666,7 @@ def create_credential_session(
|
||||
host=host,
|
||||
ip=ip,
|
||||
user_agent=user_agent,
|
||||
expiry=now + SESSION_LIFETIME,
|
||||
validated=now,
|
||||
)
|
||||
user_str = str(user_uuid)
|
||||
with _db.transaction("create_credential_session", user=user_str):
|
||||
|
||||
@@ -364,7 +364,7 @@ class Credential(msgspec.Struct, dict=True):
|
||||
class Session(msgspec.Struct, dict=True, omit_defaults=True):
|
||||
"""Session data structure.
|
||||
|
||||
Mutable fields: expiry (updated on session refresh)
|
||||
Mutable fields: validated (updated on session refresh)
|
||||
Immutable fields: user_uuid, credential_uuid, host, ip, user_agent, client_uuid
|
||||
key is the hashed db_key, stored in the dict key, not in the struct.
|
||||
|
||||
@@ -379,7 +379,7 @@ class Session(msgspec.Struct, dict=True, omit_defaults=True):
|
||||
host: str
|
||||
ip: str
|
||||
user_agent: str
|
||||
expiry: datetime
|
||||
validated: datetime
|
||||
client_uuid: UUID | None = msgspec.field(name="client", default=None)
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -401,7 +401,7 @@ class Session(msgspec.Struct, dict=True, omit_defaults=True):
|
||||
return {
|
||||
"ip": self.ip,
|
||||
"user_agent": self.user_agent,
|
||||
"expiry": self.expiry.isoformat(),
|
||||
"validated": self.validated.isoformat(),
|
||||
}
|
||||
|
||||
def store(self, last_seen: datetime) -> None:
|
||||
@@ -428,7 +428,7 @@ class Session(msgspec.Struct, dict=True, omit_defaults=True):
|
||||
host: str,
|
||||
ip: str,
|
||||
user_agent: str,
|
||||
expiry: datetime,
|
||||
validated: datetime,
|
||||
client: UUID | None = None,
|
||||
) -> Session:
|
||||
"""Create a new Session with the provided key.
|
||||
@@ -451,7 +451,7 @@ class Session(msgspec.Struct, dict=True, omit_defaults=True):
|
||||
host=host,
|
||||
ip=ip,
|
||||
user_agent=user_agent,
|
||||
expiry=expiry,
|
||||
validated=validated,
|
||||
client_uuid=client,
|
||||
)
|
||||
session.key = key
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi.responses import JSONResponse
|
||||
|
||||
from paskia import aaguid as aaguid_mod
|
||||
from paskia import db
|
||||
from paskia.authsession import EXPIRES, reset_expires
|
||||
from paskia.authsession import reset_expires
|
||||
from paskia.db import Org as OrgDC
|
||||
from paskia.db import Permission as PermDC
|
||||
from paskia.db import Role as RoleDC
|
||||
@@ -545,7 +545,6 @@ async def admin_get_user_detail(
|
||||
s,
|
||||
current_key=ctx.session.key,
|
||||
normalized_host=normalized_host,
|
||||
expires_delta=EXPIRES,
|
||||
)
|
||||
for s in user.sessions
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ from fastapi.security import HTTPBearer
|
||||
|
||||
from paskia import authcode, db
|
||||
from paskia._version import __version__
|
||||
from paskia.authsession import EXPIRES, expires, get_reset
|
||||
from paskia.authsession import EXPIRES, get_reset
|
||||
from paskia.fastapi import authz, session, user
|
||||
from paskia.fastapi.response import MsgspecResponse
|
||||
from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME, get_client_ip
|
||||
@@ -89,13 +89,13 @@ async def validate_token(
|
||||
raise
|
||||
renewed = False
|
||||
if auth:
|
||||
consumed = EXPIRES - (ctx.session.expiry - datetime.now(UTC))
|
||||
consumed = datetime.now(UTC) - ctx.session.validated
|
||||
if not timedelta(0) < consumed < _REFRESH_INTERVAL:
|
||||
db.update_session(
|
||||
ctx.session.key,
|
||||
ip=get_client_ip(request),
|
||||
user_agent=request.headers.get("user-agent") or "",
|
||||
expiry=expires(),
|
||||
validated=datetime.now(UTC),
|
||||
ctx=ctx,
|
||||
)
|
||||
session.set_session_cookie(response, auth)
|
||||
@@ -152,11 +152,7 @@ async def forward_authentication(
|
||||
"Remote-Role": str(ctx.role.uuid),
|
||||
"Remote-Role-Name": ctx.role.display_name,
|
||||
"Remote-Session-Expires": (
|
||||
ctx.session.expiry.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
||||
if ctx.session.expiry.tzinfo
|
||||
else ctx.session.expiry.replace(tzinfo=UTC)
|
||||
.isoformat()
|
||||
.replace("+00:00", "Z")
|
||||
(ctx.session.validated + EXPIRES).isoformat().replace("+00:00", "Z")
|
||||
),
|
||||
"Remote-Credential": str(ctx.session.credential),
|
||||
}
|
||||
|
||||
+2
-10
@@ -21,7 +21,6 @@ from fastapi.responses import JSONResponse
|
||||
from fastapi.security import HTTPBearer
|
||||
|
||||
from paskia import authcode, db
|
||||
from paskia.config import SESSION_LIFETIME
|
||||
from paskia.db.structs import Session
|
||||
from paskia.util import oidjwt
|
||||
from paskia.util.crypto import hash_secret
|
||||
@@ -280,14 +279,6 @@ async def _handle_refresh_token(
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Check session not expired
|
||||
now = datetime.now(UTC)
|
||||
if session.expiry < now:
|
||||
return JSONResponse(
|
||||
{"error": "invalid_grant", "error_description": "Refresh token expired"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Get user
|
||||
user = db.data().users.get(session.user_uuid)
|
||||
if not user:
|
||||
@@ -297,6 +288,7 @@ async def _handle_refresh_token(
|
||||
)
|
||||
|
||||
# Refresh the session - extend expiry and record IP/user_agent
|
||||
now = datetime.now(UTC)
|
||||
ip = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
||||
if not ip:
|
||||
ip = request.client.host if request.client else ""
|
||||
@@ -306,7 +298,7 @@ async def _handle_refresh_token(
|
||||
session.key,
|
||||
ip=ip,
|
||||
user_agent=user_agent,
|
||||
expiry=now + SESSION_LIFETIME,
|
||||
validated=now,
|
||||
)
|
||||
|
||||
_logger.info("OIDC session refreshed: %s", session.key)
|
||||
|
||||
@@ -8,7 +8,6 @@ from fastapi import FastAPI, WebSocket
|
||||
from paskia import authcode, db
|
||||
from paskia.authcode import CookieCode, OIDCCode
|
||||
from paskia.authsession import get_reset
|
||||
from paskia.config import SESSION_LIFETIME
|
||||
from paskia.db.structs import Session
|
||||
from paskia.fastapi import authz, remote
|
||||
from paskia.fastapi.session import AUTH_COOKIE, infodict
|
||||
@@ -222,7 +221,7 @@ async def websocket_authenticate(
|
||||
host=normalized_host,
|
||||
ip=metadata["ip"],
|
||||
user_agent=metadata["user_agent"],
|
||||
expiry=now + SESSION_LIFETIME,
|
||||
validated=now,
|
||||
client=oidc_client.uuid,
|
||||
)
|
||||
db.oidc_login(
|
||||
|
||||
@@ -99,7 +99,7 @@ class ApiUserSession(msgspec.Struct, omit_defaults=True):
|
||||
host: str
|
||||
ip: str
|
||||
user_agent: str
|
||||
expiry: datetime
|
||||
validated: datetime
|
||||
last_renewed: datetime
|
||||
is_current: bool = False
|
||||
is_current_host: bool = False
|
||||
@@ -113,7 +113,6 @@ class ApiUserSession(msgspec.Struct, omit_defaults=True):
|
||||
*,
|
||||
current_key: str,
|
||||
normalized_host: str | None,
|
||||
expires_delta, # timedelta
|
||||
) -> ApiUserSession:
|
||||
client_name = None
|
||||
if s.client_uuid:
|
||||
@@ -124,8 +123,8 @@ class ApiUserSession(msgspec.Struct, omit_defaults=True):
|
||||
host=s.host,
|
||||
ip=s.ip,
|
||||
user_agent=useragent.compact_user_agent(s.user_agent),
|
||||
expiry=s.expiry,
|
||||
last_renewed=s.expiry - expires_delta,
|
||||
validated=s.validated,
|
||||
last_renewed=s.validated,
|
||||
is_current=s.key == current_key,
|
||||
is_current_host=not s.client_uuid
|
||||
and bool(normalized_host and s.host and s.host == normalized_host),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from paskia.authsession import EXPIRES
|
||||
from paskia.db import SessionContext
|
||||
from paskia.util.timeutil import parse_duration
|
||||
|
||||
@@ -32,7 +31,7 @@ def check_session_age(ctx: SessionContext, max_age: str | None) -> bool:
|
||||
if ctx.credential and ctx.credential.last_used:
|
||||
auth_time = ctx.credential.last_used
|
||||
else:
|
||||
auth_time = ctx.session.expiry - EXPIRES
|
||||
auth_time = ctx.session.validated
|
||||
|
||||
time_since_auth = datetime.now(UTC) - auth_time
|
||||
return time_since_auth <= max_age_delta
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""User information formatting and retrieval logic."""
|
||||
|
||||
from paskia import aaguid, db
|
||||
from paskia.authsession import EXPIRES
|
||||
from paskia.db import SessionContext
|
||||
from paskia.util import hostutil
|
||||
from paskia.util.apistructs import (
|
||||
@@ -50,7 +49,6 @@ async def build_user_info(
|
||||
s,
|
||||
current_key=session_key,
|
||||
normalized_host=normalized_host,
|
||||
expires_delta=EXPIRES,
|
||||
)
|
||||
for s in user.sessions
|
||||
}
|
||||
|
||||
+1
-1
@@ -277,7 +277,7 @@ def create_test_session(
|
||||
host=host,
|
||||
ip=ip,
|
||||
user_agent=user_agent,
|
||||
expiry=now + duration,
|
||||
validated=now,
|
||||
)
|
||||
if session.key in ops_db._db.sessions:
|
||||
raise ValueError("Session already exists")
|
||||
|
||||
Reference in New Issue
Block a user