From 2ec6314264c241bab6e83d780b7ba84cc3cb770e Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 23 Jan 2026 20:53:03 +0000 Subject: [PATCH] Simplify session and reset token formats; removes the token utility functions entirely. --- paskia/authsession.py | 10 +++--- paskia/bootstrap.py | 4 +-- paskia/db/__init__.py | 1 + paskia/db/operations.py | 68 ++++++++++++++++++++++++++++---------- paskia/db/structs.py | 4 +-- paskia/fastapi/admin.py | 33 +++++++++--------- paskia/fastapi/api.py | 3 +- paskia/fastapi/remote.py | 14 +++----- paskia/fastapi/reset.py | 3 +- paskia/fastapi/user.py | 18 +++------- paskia/fastapi/ws.py | 9 ++--- paskia/migrate/__init__.py | 20 +++++++++-- paskia/util/permutil.py | 3 +- paskia/util/tokens.py | 44 ------------------------ paskia/util/userinfo.py | 8 ++--- 15 files changed, 112 insertions(+), 130 deletions(-) delete mode 100644 paskia/util/tokens.py diff --git a/paskia/authsession.py b/paskia/authsession.py index a8af2e3..bacc0ed 100644 --- a/paskia/authsession.py +++ b/paskia/authsession.py @@ -14,9 +14,7 @@ from uuid import UUID from paskia import db from paskia.config import SESSION_LIFETIME from paskia.db import ResetToken, Session -from paskia.globals import passkey from paskia.util import hostutil -from paskia.util.tokens import create_token, reset_key, session_key EXPIRES = SESSION_LIFETIME @@ -33,7 +31,7 @@ def reset_expires() -> datetime: async def get_reset(token: str) -> ResetToken: """Validate a credential reset token.""" - record = db.get_reset_token(reset_key(token)) + record = db.get_reset_token(token) if record: return record raise ValueError("This authentication link is no longer valid.") @@ -44,7 +42,7 @@ async def get_session(token: str, host: str | None = None) -> Session: host = hostutil.normalize_host(host) if not host: raise ValueError("Invalid host") - session = db.get_session(session_key(token)) + session = db.get_session(token) if session: if session.host is None: # First time binding: store exact host:port (or IPv6 form) now. @@ -58,11 +56,11 @@ async def get_session(token: str, host: str | None = None) -> Session: async def refresh_session_token(token: str, *, ip: str, user_agent: str): """Refresh a session extending its expiry.""" - session_record = db.get_session(session_key(token)) + session_record = db.get_session(token) if not session_record: raise ValueError("Session not found or expired") updated = db.update_session( - session_key(token), + token, ip=ip, user_agent=user_agent, expiry=expires(), diff --git a/paskia/bootstrap.py b/paskia/bootstrap.py index 041e784..98880cd 100644 --- a/paskia/bootstrap.py +++ b/paskia/bootstrap.py @@ -14,7 +14,7 @@ import uuid7 from paskia import authsession, db from paskia.db import Org, Permission, Role, User -from paskia.util import hostutil, passphrase, tokens +from paskia.util import hostutil, passphrase def _init_logger() -> logging.Logger: @@ -44,7 +44,7 @@ async def _create_and_log_admin_reset_link(user_uuid, message, session_type) -> expiry = authsession.reset_expires() db.create_reset_token( user_uuid=user_uuid, - key=tokens.reset_key(token), + passphrase=token, expiry=expiry, token_type=session_type, ) diff --git a/paskia/db/__init__.py b/paskia/db/__init__.py index f6d4dc8..7d1e534 100644 --- a/paskia/db/__init__.py +++ b/paskia/db/__init__.py @@ -76,6 +76,7 @@ from paskia.db.operations import ( remove_permission_from_organization, remove_permission_from_role, rename_permission, + set_session_host, update_credential_sign_count, update_organization_name, update_permission, diff --git a/paskia/db/operations.py b/paskia/db/operations.py index c20c995..fff966a 100644 --- a/paskia/db/operations.py +++ b/paskia/db/operations.py @@ -6,7 +6,9 @@ Context lookup: get_session_context() returns full SessionContext with effective Write operations: Functions that validate and commit, or raise ValueError. """ +import hashlib import os +import secrets from collections import deque from contextlib import contextmanager from datetime import datetime, timezone @@ -41,6 +43,7 @@ from paskia.db.structs import ( _SessionData, _UserData, ) +from paskia.util.passphrase import is_well_formed as _is_passphrase # msgspec encoder/decoder _json_encoder = msgspec.json.Encoder() @@ -174,7 +177,7 @@ def build_credential(uuid: UUID) -> Credential: ) -def build_session(key: bytes) -> Session: +def build_session(key: str) -> Session: s = _db._data.sessions[key] return Session( key=key, @@ -331,7 +334,7 @@ def get_credentials_by_user_uuid(user_uuid: str | UUID) -> list[Credential]: ] -def get_session(key: bytes) -> Session | None: +def get_session(key: str) -> Session | None: """Get session by key.""" if key not in _db._data.sessions: return None @@ -353,8 +356,20 @@ def list_sessions_for_user(user_uuid: str | UUID) -> list[Session]: ] -def get_reset_token(key: bytes) -> ResetToken | None: - """Get reset token by key.""" +def _reset_key(passphrase: str) -> bytes: + """Hash a passphrase to bytes for reset token storage.""" + if not _is_passphrase(passphrase): + raise ValueError( + "Trying to reset with a session token in place of a passphrase" + if len(passphrase) == 16 + else "Invalid passphrase format" + ) + return hashlib.sha512(passphrase.encode()).digest()[:9] + + +def get_reset_token(passphrase: str) -> ResetToken | None: + """Get reset token by passphrase.""" + key = _reset_key(passphrase) if key not in _db._data.reset_tokens: return None t = _db._data.reset_tokens[key] @@ -369,12 +384,12 @@ def get_reset_token(key: bytes) -> ResetToken | None: def get_session_context( - session_key: bytes, host: str | None = None + session_key: str, host: str | None = None ) -> SessionContext | None: """Get full session context with effective permissions. Args: - session_key: The session key bytes + session_key: The session key string host: Optional host for binding/validation and domain-scoped permissions Returns: @@ -832,7 +847,7 @@ def delete_credential( def create_session( - key: bytes, + key: str, user_uuid: UUID, credential_uuid: UUID, host: str | None, @@ -860,7 +875,8 @@ def create_session( def update_session( - key: bytes, + key: str, + host: str | None = None, ip: str | None = None, user_agent: str | None = None, expiry: datetime | None = None, @@ -871,6 +887,8 @@ def update_session( raise ValueError("Session not found") with _db.transaction(actor): s = _db._data.sessions[key] + if host is not None: + s.host = host if ip is not None: s.ip = ip if user_agent is not None: @@ -879,7 +897,12 @@ def update_session( s.expiry = expiry -def delete_session(key: bytes, actor: str = "system") -> None: +def set_session_host(key: str, host: str, actor: str = "system") -> None: + """Set the host for a session (first-time binding).""" + update_session(key, host=host, actor=actor) + + +def delete_session(key: str, actor: str = "system") -> None: """Delete a session.""" if key not in _db._data.sessions: raise ValueError("Session not found") @@ -898,13 +921,14 @@ def delete_sessions_for_user(user_uuid: str | UUID, actor: str = "system") -> No def create_reset_token( - key: bytes, + passphrase: str, user_uuid: UUID, expiry: datetime, token_type: str, actor: str = "system", ) -> None: - """Create a reset token.""" + """Create a reset token from a passphrase.""" + key = _reset_key(passphrase) if key in _db._data.reset_tokens: raise ValueError("Reset token already exists") if user_uuid not in _db._data.users: @@ -951,15 +975,21 @@ def cleanup_expired(actor: str = "system") -> int: # ------------------------------------------------------------------------- +def _create_token() -> str: + """Generate a 16-character session token using standard base64.""" + import base64 + + return base64.b64encode(secrets.token_bytes(12)).decode() + + def login( user_uuid: str | UUID, credential: Credential, - session_key: bytes, host: str | None, ip: str | None, user_agent: str | None, expiry: datetime, -) -> None: +) -> str: """Update user/credential on login and create session in a single transaction. Updates: @@ -969,6 +999,7 @@ def login( - new session Actor is set to the user UUID being logged in. + Returns the generated session token. """ if isinstance(user_uuid, str): user_uuid = UUID(user_uuid) @@ -977,9 +1008,8 @@ def login( raise ValueError(f"User {user_uuid} not found") if credential.uuid not in _db._data.credentials: raise ValueError(f"Credential {credential.uuid} not found") - if session_key in _db._data.sessions: - raise ValueError("Session already exists") + session_key = _create_token() actor = str(user_uuid) with _db.transaction(actor): # Update user @@ -997,19 +1027,19 @@ def login( user_agent=user_agent, expiry=expiry, ) + return session_key def create_credential_session( user_uuid: UUID, credential: Credential, - session_key: bytes, host: str | None, ip: str | None, user_agent: str | None, display_name: str | None = None, reset_key: bytes | None = None, actor: str = "system", -) -> None: +) -> str: """Create a credential and session together, optionally consuming a reset token. Used during registration to atomically: @@ -1017,11 +1047,14 @@ def create_credential_session( 2. Create the credential 3. Create the session 4. Delete the reset token if provided + + Returns the generated session token. """ from paskia.config import SESSION_LIFETIME now = datetime.now(timezone.utc) expiry = now + SESSION_LIFETIME + session_key = _create_token() if user_uuid not in _db._data.users: raise ValueError(f"User {user_uuid} not found") @@ -1057,3 +1090,4 @@ def create_credential_session( if reset_key: if reset_key in _db._data.reset_tokens: del _db._data.reset_tokens[reset_key] + return session_key diff --git a/paskia/db/structs.py b/paskia/db/structs.py index eefb23a..b99f1ae 100644 --- a/paskia/db/structs.py +++ b/paskia/db/structs.py @@ -47,7 +47,7 @@ class Credential(msgspec.Struct): class Session(msgspec.Struct): - key: bytes + key: str user_uuid: UUID credential_uuid: UUID host: str | None @@ -143,6 +143,6 @@ class _DatabaseData(msgspec.Struct, omit_defaults=True): roles: dict[UUID, _RoleData] users: dict[UUID, _UserData] credentials: dict[UUID, _CredentialData] - sessions: dict[bytes, _SessionData] + sessions: dict[str, _SessionData] reset_tokens: dict[bytes, _ResetTokenData] v: int = 0 diff --git a/paskia/fastapi/admin.py b/paskia/fastapi/admin.py index 221df10..064b318 100644 --- a/paskia/fastapi/admin.py +++ b/paskia/fastapi/admin.py @@ -15,10 +15,8 @@ from paskia.util import ( passphrase, permutil, querysafe, - tokens, useragent, ) -from paskia.util.tokens import encode_session_key, session_key app = FastAPI() @@ -217,7 +215,9 @@ async def admin_add_org_permission( ctx = await authz.verify( auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all ) - db.add_permission_to_organization(str(org_uuid), permission_id, actor=str(ctx.user.uuid)) + db.add_permission_to_organization( + str(org_uuid), permission_id, actor=str(ctx.user.uuid) + ) return {"status": "ok"} @@ -241,7 +241,9 @@ async def admin_remove_org_permission( "This would lock you out of admin access." ) - db.remove_permission_from_organization(str(org_uuid), permission_id, actor=str(ctx.user.uuid)) + db.remove_permission_from_organization( + str(org_uuid), permission_id, actor=str(ctx.user.uuid) + ) return {"status": "ok"} @@ -543,7 +545,7 @@ async def admin_create_user_registration_link( expiry = reset_expires() db.create_reset_token( user_uuid=user_uuid, - key=tokens.reset_key(token), + passphrase=token, expiry=expiry, token_type=token_type, actor=str(ctx.user.uuid), @@ -640,13 +642,13 @@ async def admin_get_user_detail( # Get sessions for the user normalized_request_host = hostutil.normalize_host(request.headers.get("host")) session_records = db.list_sessions_for_user(user_uuid) - current_session_key = session_key(auth) + current_session_key = auth sessions_payload: list[dict] = [] for entry in session_records: renewed = entry.expiry - EXPIRES sessions_payload.append( { - "id": encode_session_key(entry.key), + "id": entry.key, "credential_uuid": str(entry.credential_uuid), "host": entry.host, "ip": entry.ip, @@ -787,21 +789,14 @@ async def admin_delete_user_session( status_code=403, detail="Insufficient permissions", mode="forbidden" ) - try: - target_key = tokens.decode_session_key(session_id) - except ValueError as exc: - raise HTTPException( - status_code=400, detail="Invalid session identifier" - ) from exc - - target_session = db.get_session(target_key) + target_session = db.get_session(session_id) if not target_session or target_session.user_uuid != user_uuid: raise HTTPException(status_code=404, detail="Session not found") - db.delete_session(target_key, actor=str(ctx.user.uuid)) + db.delete_session(session_id, actor=str(ctx.user.uuid)) # Check if admin terminated their own session - current_terminated = target_key == session_key(auth) + current_terminated = session_id == auth return {"status": "ok", "current_session_terminated": current_terminated} @@ -1047,7 +1042,9 @@ async def admin_rename_permission( _check_admin_lockout(str(perm.uuid), domain_value, request.headers.get("host")) # All current backends support rename_permission - db.rename_permission(old_scope, new_scope, display_name, domain_value, actor=str(ctx.user.uuid)) + db.rename_permission( + old_scope, new_scope, display_name, domain_value, actor=str(ctx.user.uuid) + ) return {"status": "ok"} diff --git a/paskia/fastapi/api.py b/paskia/fastapi/api.py index 6ce6f79..b6e20be 100644 --- a/paskia/fastapi/api.py +++ b/paskia/fastapi/api.py @@ -24,7 +24,6 @@ from paskia.fastapi import authz, session, user from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME from paskia.globals import passkey as global_passkey from paskia.util import frontend, hostutil, htmlutil, passphrase, userinfo -from paskia.util.tokens import session_key bearer_auth = HTTPBearer(auto_error=True) @@ -293,7 +292,7 @@ async def api_logout(request: Request, response: Response, auth=AUTH_COOKIE): except ValueError: return {"message": "Already logged out"} with suppress(Exception): - db.delete_session(session_key(auth), actor=str(s.user_uuid)) + db.delete_session(auth, actor=str(s.user_uuid)) session.clear_session_cookie(response) return {"message": "Logged out successfully"} diff --git a/paskia/fastapi/remote.py b/paskia/fastapi/remote.py index e97cffd..0263270 100644 --- a/paskia/fastapi/remote.py +++ b/paskia/fastapi/remote.py @@ -342,25 +342,23 @@ async def websocket_remote_auth_permit(ws: WebSocket): if request.action == "register": # For registration, create a reset token for device addition from paskia.authsession import expires - from paskia.util import hostutil, tokens + from paskia.util import hostutil token_str = passphrase.generate() expiry = expires() db.create_reset_token( user_uuid=stored_cred.user_uuid, - key=tokens.reset_key(token_str), + passphrase=token_str, expiry=expiry, token_type="device addition", actor=str(stored_cred.user_uuid), ) reset_token = token_str # Also create a session so the device is logged in - session_token = passphrase.generate() normalized_host = hostutil.normalize_host(request.host) - db.login( + session_token = db.login( user_uuid=stored_cred.user_uuid, credential=stored_cred, - session_key=tokens.session_key(session_token), host=normalized_host, ip=request.ip, user_agent=request.user_agent, @@ -369,14 +367,12 @@ async def websocket_remote_auth_permit(ws: WebSocket): else: # Default login action from paskia.authsession import expires - from paskia.util import hostutil, tokens + from paskia.util import hostutil - session_token = passphrase.generate() normalized_host = hostutil.normalize_host(request.host) - db.login( + session_token = db.login( user_uuid=stored_cred.user_uuid, credential=stored_cred, - session_key=tokens.session_key(session_token), host=normalized_host, ip=request.ip, user_agent=request.user_agent, diff --git a/paskia/fastapi/reset.py b/paskia/fastapi/reset.py index b42dc59..baacd8e 100644 --- a/paskia/fastapi/reset.py +++ b/paskia/fastapi/reset.py @@ -18,7 +18,6 @@ from uuid import UUID from paskia import authsession as _authsession from paskia import db as _db from paskia.util import hostutil, passphrase -from paskia.util import tokens as _tokens async def _resolve_targets(query: str | None): @@ -65,7 +64,7 @@ async def _create_reset(user, role_name: str): token = passphrase.generate() expiry = _authsession.reset_expires() _db.create_reset_token( - key=_tokens.reset_key(token), + passphrase=token, user_uuid=user.uuid, expiry=expiry, token_type="manual reset", diff --git a/paskia/fastapi/user.py b/paskia/fastapi/user.py index 2d4a16e..7d1b9af 100644 --- a/paskia/fastapi/user.py +++ b/paskia/fastapi/user.py @@ -18,8 +18,7 @@ from paskia.authsession import ( ) from paskia.fastapi import authz, session from paskia.fastapi.session import AUTH_COOKIE -from paskia.util import hostutil, passphrase, tokens -from paskia.util.tokens import decode_session_key, session_key +from paskia.util import hostutil, passphrase app = FastAPI() @@ -92,19 +91,12 @@ async def api_delete_session( status_code=401, detail="Session expired", mode="login" ) from exc - try: - target_key = decode_session_key(session_id) - except ValueError as exc: - raise HTTPException( - status_code=400, detail="Invalid session identifier" - ) from exc - - target_session = db.get_session(target_key) + target_session = db.get_session(session_id) if not target_session or target_session.user_uuid != current_session.user_uuid: raise HTTPException(status_code=404, detail="Session not found") - db.delete_session(target_key, actor=str(current_session.user_uuid)) - current_terminated = target_key == session_key(auth) + db.delete_session(session_id, actor=str(current_session.user_uuid)) + current_terminated = session_id == auth if current_terminated: session.clear_session_cookie(response) # explicit because 200 return {"status": "ok", "current_session_terminated": current_terminated} @@ -146,7 +138,7 @@ async def api_create_link( expiry = expires() db.create_reset_token( user_uuid=s.user_uuid, - key=tokens.reset_key(token), + passphrase=token, expiry=expiry, token_type="device addition", actor=str(s.user_uuid), diff --git a/paskia/fastapi/ws.py b/paskia/fastapi/ws.py index 83d75ad..ccd3e9d 100644 --- a/paskia/fastapi/ws.py +++ b/paskia/fastapi/ws.py @@ -9,7 +9,6 @@ from paskia.fastapi.session import AUTH_COOKIE, infodict from paskia.fastapi.wsutil import validate_origin, websocket_error_handler from paskia.globals import passkey from paskia.util import hostutil, passphrase -from paskia.util.tokens import create_token, session_key # Create a FastAPI subapp for WebSocket endpoints app = FastAPI() @@ -78,13 +77,11 @@ async def websocket_register_add( credential = await register_chat(ws, user_uuid, user_name, origin, challenge_ids) # Create a new session and store everything in database - token = create_token() metadata = infodict(ws, "authenticated") - db.create_credential_session( # type: ignore[attr-defined] + token = db.create_credential_session( # type: ignore[attr-defined] user_uuid=user_uuid, credential=credential, reset_key=(s.key if reset is not None else None), - session_key=session_key(token), display_name=user_name, host=host, ip=metadata.get("ip"), @@ -145,7 +142,6 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE): # Create session and update user/credential in a single transaction assert stored_cred.uuid is not None metadata = infodict(ws, "auth") - token = create_token() normalized_host = hostutil.normalize_host(host) if not normalized_host: raise ValueError("Host required for session creation") @@ -154,10 +150,9 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE): if not (hostname == rp_id or hostname.endswith(f".{rp_id}")): raise ValueError(f"Host must be the same as or a subdomain of {rp_id}") - db.login( + token = db.login( user_uuid=stored_cred.user_uuid, credential=stored_cred, - session_key=session_key(token), host=normalized_host, ip=metadata.get("ip") or "", user_agent=metadata.get("user_agent") or "", diff --git a/paskia/migrate/__init__.py b/paskia/migrate/__init__.py index f45030e..63a5c86 100644 --- a/paskia/migrate/__init__.py +++ b/paskia/migrate/__init__.py @@ -15,6 +15,8 @@ import asyncio from datetime import datetime, timezone from uuid import UUID +import base64url + from paskia.authsession import EXPIRES from .sql import ( @@ -194,12 +196,19 @@ async def migrate_from_sql( print(f" Migrated {len(cred_models)} credentials") # Migrate sessions + # Old format: b"sess" + 12 bytes -> New format: base64url string (16 chars) async with sql_db.session() as session: result = await session.execute(select(SessionModel)) session_models = result.scalars().all() for sm in session_models: sess = sm.as_dataclass() - session_key: bytes = sess.key + old_key: bytes = sess.key + # Strip b"sess" prefix and encode remaining 12 bytes as base64url + if old_key.startswith(b"sess"): + session_key = base64url.enc(old_key[4:]) + else: + # Already in new format or unknown - try to use as-is + session_key = base64url.enc(old_key[:12]) json_db._data.sessions[session_key] = _SessionData( user=sess.user_uuid, credential=sess.credential_uuid, @@ -211,12 +220,19 @@ async def migrate_from_sql( print(f" Migrated {len(session_models)} sessions") # Migrate reset tokens + # Old format: b"rset" + 16 bytes hash -> New format: 9 bytes (truncated hash) async with sql_db.session() as session: result = await session.execute(select(ResetTokenModel)) token_models = result.scalars().all() for tm in token_models: token = tm.as_dataclass() - token_key: bytes = token.key + old_key: bytes = token.key + # Strip b"rset" prefix and take first 9 bytes of hash + if old_key.startswith(b"rset"): + token_key = old_key[4:13] # 9 bytes after prefix + else: + # Already in new format or unknown - truncate to 9 bytes + token_key = old_key[:9] json_db._data.reset_tokens[token_key] = _ResetTokenData( user=token.user_uuid, expiry=token.expiry, diff --git a/paskia/util/permutil.py b/paskia/util/permutil.py index d4f883f..91bafe7 100644 --- a/paskia/util/permutil.py +++ b/paskia/util/permutil.py @@ -5,7 +5,6 @@ from fnmatch import fnmatchcase from paskia import db from paskia.util.hostutil import normalize_host -from paskia.util.tokens import session_key __all__ = ["has_any", "has_all", "session_context"] @@ -41,4 +40,4 @@ async def session_context(auth: str | None, host: str | None = None): if not auth: return None normalized_host = normalize_host(host) if host else None - return db.get_session_context(session_key(auth), normalized_host) + return db.get_session_context(auth, normalized_host) diff --git a/paskia/util/tokens.py b/paskia/util/tokens.py deleted file mode 100644 index a702819..0000000 --- a/paskia/util/tokens.py +++ /dev/null @@ -1,44 +0,0 @@ -import hashlib -import secrets - -import base64url - -from paskia.util.passphrase import is_well_formed - - -def create_token() -> str: - return secrets.token_urlsafe(12) # 16 characters Base64 - - -def session_key(token: str) -> bytes: - if len(token) != 16: - raise ValueError("Session token must be exactly 16 characters long") - return b"sess" + base64url.dec(token) - - -def encode_session_key(key: bytes) -> str: - """Encode an opaque session key for external representation.""" - return base64url.enc(key) - - -def decode_session_key(encoded: str) -> bytes: - """Decode an opaque session key from its public representation.""" - if not encoded: - raise ValueError("Invalid session identifier") - try: - raw = base64url.dec(encoded) - except Exception as exc: # pragma: no cover - defensive - raise ValueError("Invalid session identifier") from exc - if not raw.startswith(b"sess"): - raise ValueError("Invalid session identifier") - return raw - - -def reset_key(passphrase: str) -> bytes: - if not is_well_formed(passphrase): - raise ValueError( - "Trying to reset with a session token in place of a passphrase" - if len(passphrase) == 16 - else "Invalid passphrase format" - ) - return b"rset" + hashlib.sha512(passphrase.encode()).digest()[:12] diff --git a/paskia/util/userinfo.py b/paskia/util/userinfo.py index 03b2717..cb705b3 100644 --- a/paskia/util/userinfo.py +++ b/paskia/util/userinfo.py @@ -3,8 +3,8 @@ from datetime import timezone from paskia import aaguid, db -from paskia.authsession import EXPIRES, session_key -from paskia.util import hostutil, permutil, tokens, useragent +from paskia.authsession import EXPIRES +from paskia.util import hostutil, permutil, useragent def _format_datetime(dt): @@ -87,13 +87,13 @@ async def format_user_info( # Format sessions normalized_request_host = hostutil.normalize_host(request_host) session_records = db.list_sessions_for_user(user_uuid) - current_session_key = session_key(auth) + current_session_key = auth sessions_payload: list[dict] = [] for entry in session_records: sessions_payload.append( { - "id": tokens.encode_session_key(entry.key), + "id": entry.key, "credential_uuid": str(entry.credential_uuid), "host": entry.host, "ip": entry.ip,