feat(2-Schritt1): Additiver Ein-Klick-Login per Capability-Token

Zweiter Auth-Weg NEBEN Authelia (nichts schaltet um, kein Aussperr-Risiko):
- Link https://voice.jamulix.de/?k=<token> -> Token landet im va_token-Cookie
  (HttpOnly, Secure, SameSite=Lax, 1 Jahr), Token wird aus der URL entfernt.
- Cookie/?k= authentifiziert App-Routen (require_user) und WS (/ws/voice,
  /ws/chat). Token = vorhandenes Nutzer-Token (get_user_by_token).
- authenticate(): fehlt der SSO-Header, wird auf Token-Auth durchgefallen
  (statt hartem 401) — Voraussetzung für die spätere nginx-Trennung.

Authelia/Forward-Auth bleibt voll funktionsfähig (Live verifiziert: beide
Wege gehen). Tests: Cookie-, ?k=- und Negativ-Pfad. 217 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-25 04:24:52 +02:00
commit 47daf918ae
4 changed files with 88 additions and 10 deletions

View file

@ -33,7 +33,7 @@ from app.store import ANONYMOUS_USER_ID, SessionOwnershipError
from app.audio.vad import EnergyVAD
from app.core.memory_extractor import maybe_schedule_extraction
from app.quota import enforce_quota, record_usage, QuotaExceededError
from app.auth import authenticate
from app.auth import authenticate, _cookie_value, CAPABILITY_COOKIE
router = APIRouter()
@ -47,11 +47,20 @@ _OVERRIDE_KEYS = (
)
def _capability_token(websocket: WebSocket, token: str | None) -> str | None:
"""Token aus ?token=, ?k= oder dem va_token-Cookie (Senioren-Login)."""
return (
token
or websocket.query_params.get("k")
or _cookie_value(websocket.headers.get("cookie"), CAPABILITY_COOKIE)
)
def _authenticate(websocket: WebSocket, token: str | None):
# Forward-Auth (SSO) greift auch beim WS-Handshake: SSOwat injiziert den
# Identitaets-Header in den Upgrade-Request -> aus websocket.headers lesbar.
client_host = websocket.client.host if websocket.client else ""
return authenticate(websocket.headers, client_host, token)
return authenticate(websocket.headers, client_host, _capability_token(websocket, token))
async def _resolve(user, session_id, options):