Harden auth failure handling and registration logging

This commit is contained in:
Jage9
2026-02-25 01:42:24 -05:00
parent 6fe07cae3f
commit fad78e12af
2 changed files with 41 additions and 7 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import hashlib import hashlib
import hmac import hmac
import logging
from pathlib import Path from pathlib import Path
import re import re
import secrets import secrets
@@ -23,6 +24,7 @@ ARGON2_PARALLELISM = 1
ARGON2_HASH_LEN = 32 ARGON2_HASH_LEN = 32
ARGON2_SALT_LEN = 16 ARGON2_SALT_LEN = 16
USERNAME_PATTERN = re.compile(r"^[a-z0-9_-]+$") USERNAME_PATTERN = re.compile(r"^[a-z0-9_-]+$")
LOGGER = logging.getLogger("chgrid.server.auth")
def _build_dummy_password_hash(password_hasher: PasswordHasher) -> str: def _build_dummy_password_hash(password_hasher: PasswordHasher) -> str:
@@ -127,14 +129,14 @@ class AuthService:
with self._conn_lock: with self._conn_lock:
normalized_username = self._normalize_username(username) normalized_username = self._normalize_username(username)
self._validate_username(normalized_username)
self._validate_password(password)
normalized_email = self._normalize_email(email)
if role not in {"user", "admin"}:
raise AuthError("role must be user or admin.")
now_ms = self.now_ms()
password_hash = self._hash_password(password)
try: try:
self._validate_username(normalized_username)
self._validate_password(password)
normalized_email = self._normalize_email(email)
if role not in {"user", "admin"}:
raise AuthError("role must be user or admin.")
now_ms = self.now_ms()
password_hash = self._hash_password(password)
self._db_execute( self._db_execute(
""" """
INSERT INTO users ( INSERT INTO users (
@@ -147,12 +149,22 @@ class AuthService:
except sqlite3.IntegrityError as exc: except sqlite3.IntegrityError as exc:
message = str(exc).lower() message = str(exc).lower()
if "users.username" in message: if "users.username" in message:
LOGGER.warning("register rejected username_taken username=%s", normalized_username)
raise AuthError("Username is already taken.") from exc raise AuthError("Username is already taken.") from exc
if "users.email" in message: if "users.email" in message:
LOGGER.warning("register rejected email_taken username=%s", normalized_username)
raise AuthError("Email is already in use.") from exc raise AuthError("Email is already in use.") from exc
LOGGER.exception("register sqlite integrity failure username=%s", normalized_username)
raise AuthError("Registration failed due to a database constraint.") from exc
except AuthError as exc:
LOGGER.warning("register rejected username=%s reason=%s", normalized_username, str(exc))
raise raise
except Exception as exc:
LOGGER.exception("register unexpected failure username=%s", normalized_username)
raise AuthError("Registration failed due to a server error.") from exc
user = self._get_user_by_username(normalized_username) user = self._get_user_by_username(normalized_username)
if user is None: if user is None:
LOGGER.error("register created user missing username=%s", normalized_username)
raise AuthError("Failed to load newly created user.") raise AuthError("Failed to load newly created user.")
self._db_execute( self._db_execute(
""" """

View File

@@ -968,6 +968,8 @@ class SignalingServer:
) )
async for raw_message in websocket: async for raw_message in websocket:
await self._handle_message(client, raw_message) await self._handle_message(client, raw_message)
except Exception:
LOGGER.exception("client message loop error id=%s ip=%s", client.id, self._client_ip(client))
finally: finally:
if websocket in self.clients: if websocket in self.clients:
disconnected = self.clients.pop(websocket) disconnected = self.clients.pop(websocket)
@@ -1172,6 +1174,26 @@ class SignalingServer:
), ),
) )
return True return True
except Exception:
if isinstance(packet, (AuthLoginPacket, AuthRegisterPacket, AuthResumePacket)):
self._record_auth_failure(client, packet)
await self._sleep_auth_failure_jitter()
LOGGER.exception(
"auth unexpected error id=%s ip=%s packet=%s",
client.id,
self._client_ip(client),
packet.type,
)
await self._send(
client.websocket,
AuthResultPacket(
type="auth_result",
ok=False,
message="Authentication failed due to a server error. Please try again.",
authPolicy=self._auth_policy(),
),
)
return True
if isinstance(packet, (AuthLoginPacket, AuthRegisterPacket, AuthResumePacket)): if isinstance(packet, (AuthLoginPacket, AuthRegisterPacket, AuthResumePacket)):
self._clear_auth_failures(client, packet) self._clear_auth_failures(client, packet)