From fad78e12afef39b905648399b298c583817fc930 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Wed, 25 Feb 2026 01:42:24 -0500 Subject: [PATCH] Harden auth failure handling and registration logging --- server/app/auth_service.py | 26 +++++++++++++++++++------- server/app/server.py | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/server/app/auth_service.py b/server/app/auth_service.py index c160112..4d2a528 100644 --- a/server/app/auth_service.py +++ b/server/app/auth_service.py @@ -5,6 +5,7 @@ from __future__ import annotations from dataclasses import dataclass import hashlib import hmac +import logging from pathlib import Path import re import secrets @@ -23,6 +24,7 @@ ARGON2_PARALLELISM = 1 ARGON2_HASH_LEN = 32 ARGON2_SALT_LEN = 16 USERNAME_PATTERN = re.compile(r"^[a-z0-9_-]+$") +LOGGER = logging.getLogger("chgrid.server.auth") def _build_dummy_password_hash(password_hasher: PasswordHasher) -> str: @@ -127,14 +129,14 @@ class AuthService: with self._conn_lock: 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: + 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( """ INSERT INTO users ( @@ -147,12 +149,22 @@ class AuthService: except sqlite3.IntegrityError as exc: message = str(exc).lower() if "users.username" in message: + LOGGER.warning("register rejected username_taken username=%s", normalized_username) raise AuthError("Username is already taken.") from exc if "users.email" in message: + LOGGER.warning("register rejected email_taken username=%s", normalized_username) 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 + 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) if user is None: + LOGGER.error("register created user missing username=%s", normalized_username) raise AuthError("Failed to load newly created user.") self._db_execute( """ diff --git a/server/app/server.py b/server/app/server.py index 7f873bf..a6c8ec6 100644 --- a/server/app/server.py +++ b/server/app/server.py @@ -968,6 +968,8 @@ class SignalingServer: ) async for raw_message in websocket: 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: if websocket in self.clients: disconnected = self.clients.pop(websocket) @@ -1172,6 +1174,26 @@ class SignalingServer: ), ) 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)): self._clear_auth_failures(client, packet)