chore: secure dashboard proxy endpoint and decouple lan checks
Deploy Dashboard / deploy (push) Successful in 1m48s

This commit is contained in:
Gan, Jimmy
2026-02-27 23:41:36 +08:00
parent 29a9e9d881
commit 3888a57642
5 changed files with 57 additions and 14 deletions
+25 -8
View File
@@ -113,6 +113,11 @@ async def login(creds: LoginRequest, request: Request):
async def proxy_auth(request: Request):
"""Auto-login when Authelia has already authenticated the user via forward-auth.
Caddy sets Remote-User header after successful Authelia 2FA verification."""
# Prevent IP spoofing: ensure proxy traffic is from localhost (Caddy)
if request.client.host not in ("127.0.0.1", "::1"):
logger.warning(f"Rejected proxy auth attempt from unauthorized IP: {request.client.host}")
raise HTTPException(status_code=403, detail="Unauthorized proxy source")
remote_user = request.headers.get("Remote-User")
if not remote_user:
raise HTTPException(status_code=401, detail="No proxy auth header")
@@ -199,13 +204,25 @@ async def change_password(req: ChangePasswordRequest, request: Request, current_
_challenges = {}
def _store_challenge(challenge: bytes):
_challenges["current"] = (challenge, time.time())
_challenges[challenge] = time.time()
now = time.time()
expired = [k for k, v in _challenges.items() if now - v > 300]
for k in expired:
_challenges.pop(k, None)
def _get_challenge() -> bytes:
entry = _challenges.pop("current", None)
if not entry or time.time() - entry[1] > 300:
raise HTTPException(status_code=400, detail="Challenge expired")
return entry[0]
def _get_challenge(client_data_b64: str) -> bytes:
if not client_data_b64:
raise HTTPException(status_code=400, detail="Missing clientDataJSON")
try:
data = json.loads(base64url_to_bytes(client_data_b64).decode('utf-8'))
chal_bytes = base64url_to_bytes(data.get("challenge", ""))
except Exception:
raise HTTPException(status_code=400, detail="Invalid clientDataJSON")
entry = _challenges.pop(chal_bytes, None)
if not entry or time.time() - entry > 300:
raise HTTPException(status_code=400, detail="Challenge expired or invalid")
return chal_bytes
@router.post("/passkey/register/options")
async def passkey_register_options(current_user: str = Depends(auth.get_current_user)):
@@ -225,7 +242,7 @@ async def passkey_register_options(current_user: str = Depends(auth.get_current_
@router.post("/passkey/register/verify")
async def passkey_register_verify(request: Request, current_user: str = Depends(auth.get_current_user)):
body = await request.json()
challenge = _get_challenge()
challenge = _get_challenge(body.get("response", {}).get("clientDataJSON", ""))
try:
verification = verify_registration_response(
credential=body,
@@ -263,7 +280,7 @@ async def passkey_login_options(request: Request):
@limiter.limit("10/minute")
async def passkey_login_verify(request: Request):
body = await request.json()
challenge = _get_challenge()
challenge = _get_challenge(body.get("response", {}).get("clientDataJSON", ""))
creds = auth.load_passkey_credentials()
cred_id_b64 = body.get("id", "")
matched = next((c for c in creds if c["credential_id"] == cred_id_b64), None)