auth: fix WS session_id extraction and track WebAuthn success/failure
Browsers cannot send custom X-Session-Id header on WebSocket connections, so decode the token payload to extract session_id. Add WebAuthn success/failure recording to support rate limiter counter resets.
This commit is contained in:
@@ -39,6 +39,8 @@ from lib.auth import (
|
|||||||
get_access_ttl,
|
get_access_ttl,
|
||||||
record_login_failure,
|
record_login_failure,
|
||||||
record_login_success,
|
record_login_success,
|
||||||
|
record_webauthn_failure,
|
||||||
|
record_webauthn_success,
|
||||||
validate_token,
|
validate_token,
|
||||||
)
|
)
|
||||||
from lib.auth_users import (
|
from lib.auth_users import (
|
||||||
@@ -519,6 +521,7 @@ def webauthn_authenticate_finish(_request: Any, body: Any) -> dict[str, Any]:
|
|||||||
if not check_webauthn_rate(username, client_ip):
|
if not check_webauthn_rate(username, client_ip):
|
||||||
raise ValueError("Too many WebAuthn attempts. Please try again later.")
|
raise ValueError("Too many WebAuthn attempts. Please try again later.")
|
||||||
|
|
||||||
|
try:
|
||||||
verify_authentication(
|
verify_authentication(
|
||||||
username,
|
username,
|
||||||
assertion_response,
|
assertion_response,
|
||||||
@@ -526,6 +529,11 @@ def webauthn_authenticate_finish(_request: Any, body: Any) -> dict[str, Any]:
|
|||||||
origin=origin,
|
origin=origin,
|
||||||
rp_id=rp_id,
|
rp_id=rp_id,
|
||||||
)
|
)
|
||||||
|
except ValueError:
|
||||||
|
record_webauthn_failure(username, client_ip)
|
||||||
|
raise
|
||||||
|
|
||||||
|
record_webauthn_success(username, client_ip)
|
||||||
|
|
||||||
user = get_user(username)
|
user = get_user(username)
|
||||||
if user is None:
|
if user is None:
|
||||||
|
|||||||
+9
-4
@@ -373,7 +373,7 @@ async def _handle_ws(request: web.Request) -> web.Response:
|
|||||||
1. WebSocket subprotocol header (Sec-WebSocket-Protocol: "Bearer <token>")
|
1. WebSocket subprotocol header (Sec-WebSocket-Protocol: "Bearer <token>")
|
||||||
2. X-Auth-Token header (nginx-injected)
|
2. X-Auth-Token header (nginx-injected)
|
||||||
"""
|
"""
|
||||||
from lib.auth import validate_token
|
from lib.auth import decode_token, validate_token
|
||||||
|
|
||||||
token_param = None
|
token_param = None
|
||||||
|
|
||||||
@@ -392,11 +392,16 @@ async def _handle_ws(request: web.Request) -> web.Response:
|
|||||||
{"ok": False, "error": "authentication required"}, status=401
|
{"ok": False, "error": "authentication required"}, status=401
|
||||||
)
|
)
|
||||||
|
|
||||||
session_header = request.headers.get("X-Session-Id")
|
# Decode token to extract session_id from payload (browsers can't send
|
||||||
if not session_header:
|
# X-Session-Id header on WebSocket connections, only subprotocols)
|
||||||
|
raw_payload = decode_token(token_param)
|
||||||
|
if raw_payload is None:
|
||||||
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
|
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
|
||||||
|
|
||||||
payload = validate_token(
|
payload = validate_token(
|
||||||
token_param, token_type="access", session_id=session_header
|
token_param,
|
||||||
|
token_type="access",
|
||||||
|
session_id=raw_payload.get("session_id"),
|
||||||
)
|
)
|
||||||
if payload is None:
|
if payload is None:
|
||||||
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
|
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
|
||||||
|
|||||||
+14
@@ -453,3 +453,17 @@ def check_webauthn_rate(username: str, client_ip: str | None = None) -> bool:
|
|||||||
if client_ip and not _webauthn_limiter.is_allowed(client_ip):
|
if client_ip and not _webauthn_limiter.is_allowed(client_ip):
|
||||||
return False
|
return False
|
||||||
return _webauthn_limiter.is_allowed(username)
|
return _webauthn_limiter.is_allowed(username)
|
||||||
|
|
||||||
|
|
||||||
|
def record_webauthn_failure(username: str, client_ip: str | None = None) -> None:
|
||||||
|
"""Record a failed WebAuthn attempt."""
|
||||||
|
if client_ip:
|
||||||
|
_webauthn_limiter.record_failure(client_ip)
|
||||||
|
_webauthn_limiter.record_failure(username)
|
||||||
|
|
||||||
|
|
||||||
|
def record_webauthn_success(username: str, client_ip: str | None = None) -> None:
|
||||||
|
"""Record a successful WebAuthn attempt (resets failure counter)."""
|
||||||
|
if client_ip:
|
||||||
|
_webauthn_limiter.record_success(client_ip)
|
||||||
|
_webauthn_limiter.record_success(username)
|
||||||
|
|||||||
Reference in New Issue
Block a user