Session keys hardened (namespaced hashes of tokens). Various cleanup.
This commit is contained in:
+23
-29
@@ -11,6 +11,7 @@ import uuid7
|
||||
from paskia import db
|
||||
from paskia.util import hostutil
|
||||
from paskia.util import passphrase as passphrase_util
|
||||
from paskia.util.crypto import hash_secret
|
||||
|
||||
# Sentinel for uuid fields before they are set by create() or DB post init
|
||||
_UUID_UNSET = UUID(int=0)
|
||||
@@ -360,9 +361,12 @@ class Session(msgspec.Struct, dict=True, omit_defaults=True):
|
||||
|
||||
Mutable fields: expiry (updated on session refresh)
|
||||
Immutable fields: user_uuid, credential_uuid, host, ip, user_agent, client_uuid
|
||||
key is stored in the dict key, not in the struct.
|
||||
key is the hashed db_key, stored in the dict key, not in the struct.
|
||||
|
||||
If client_uuid is set, this is an OIDC session (key is the sid claim).
|
||||
If client_uuid is set, this is an OIDC session.
|
||||
|
||||
Security: The database stores only derived keys, never the raw secret.
|
||||
A database leak does not expose working session credentials.
|
||||
"""
|
||||
|
||||
user_uuid: UUID = msgspec.field(name="user")
|
||||
@@ -375,7 +379,7 @@ class Session(msgspec.Struct, dict=True, omit_defaults=True):
|
||||
|
||||
def __post_init__(self):
|
||||
if not hasattr(self, "key"):
|
||||
self.key: str = ""
|
||||
self.key: bytes = b""
|
||||
|
||||
@property
|
||||
def user(self) -> User:
|
||||
@@ -415,20 +419,27 @@ class Session(msgspec.Struct, dict=True, omit_defaults=True):
|
||||
cls,
|
||||
user: UUID | User,
|
||||
credential: UUID | Credential,
|
||||
key: bytes,
|
||||
host: str,
|
||||
ip: str,
|
||||
user_agent: str,
|
||||
expiry: datetime,
|
||||
client: UUID | None = None,
|
||||
) -> Session:
|
||||
"""Create a new Session with auto-generated key.
|
||||
"""Create a new Session with the provided key.
|
||||
|
||||
If client is provided, creates an OIDC session (key becomes sid claim).
|
||||
Args:
|
||||
key: The hashed session key (derived from secret via hash_secret)
|
||||
|
||||
Returns:
|
||||
Session object with key set
|
||||
"""
|
||||
|
||||
user_uuid = user if isinstance(user, UUID) else user.uuid
|
||||
credential_uuid = (
|
||||
credential if isinstance(credential, UUID) else credential.uuid
|
||||
)
|
||||
|
||||
session = cls(
|
||||
user_uuid=user_uuid,
|
||||
credential_uuid=credential_uuid,
|
||||
@@ -438,7 +449,7 @@ class Session(msgspec.Struct, dict=True, omit_defaults=True):
|
||||
expiry=expiry,
|
||||
client_uuid=client,
|
||||
)
|
||||
session.key = secrets.token_urlsafe(12)
|
||||
session.key = key
|
||||
return session
|
||||
|
||||
|
||||
@@ -600,7 +611,7 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False):
|
||||
roles: dict[UUID, Role] = {}
|
||||
users: dict[UUID, User] = {}
|
||||
credentials: dict[UUID, Credential] = {}
|
||||
sessions: dict[str, Session] = {}
|
||||
sessions: dict[bytes, Session] = {}
|
||||
reset_tokens: dict[bytes, ResetToken] = {}
|
||||
# OIDC provider data
|
||||
oid_clients: dict[UUID, OIDClient] = {}
|
||||
@@ -632,19 +643,21 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False):
|
||||
return self._store.transaction(action, ctx, user=user)
|
||||
|
||||
def session_ctx(
|
||||
self, session_key: str, host: str | None = None
|
||||
self, session_secret: str, host: str | None = None
|
||||
) -> SessionContext | None:
|
||||
"""Get full session context with effective permissions.
|
||||
|
||||
Args:
|
||||
session_key: The session key string
|
||||
session_secret: The session secret (cookie value) - will be hashed for lookup
|
||||
host: Optional host for binding/validation and domain-scoped permissions
|
||||
|
||||
Returns:
|
||||
SessionContext if valid, None if session not found, expired, or host mismatch
|
||||
"""
|
||||
|
||||
key = hash_secret("cookie", session_secret)
|
||||
try:
|
||||
s = self.sessions[session_key]
|
||||
s = self.sessions[key]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@@ -693,22 +706,3 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False):
|
||||
credential=credential,
|
||||
permissions=effective_perms,
|
||||
)
|
||||
|
||||
def oidc_session_by_sid(
|
||||
self, sid: str, client_uuid: UUID | None = None
|
||||
) -> Session | None:
|
||||
"""Look up an OIDC session by sid (session key).
|
||||
|
||||
Args:
|
||||
sid: The session ID (same as session key)
|
||||
client_uuid: If provided, verify the session belongs to this client
|
||||
|
||||
Returns:
|
||||
Session if found and valid OIDC session, None otherwise
|
||||
"""
|
||||
s = self.sessions.get(sid)
|
||||
if not s or s.client_uuid is None:
|
||||
return None
|
||||
if client_uuid is not None and s.client_uuid != client_uuid:
|
||||
return None
|
||||
return s
|
||||
|
||||
Reference in New Issue
Block a user