Harden auth failure handling and registration logging
This commit is contained in:
@@ -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(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user