Files
pi-pm3/app/backend/api/auth.py
michael 2ec89041ef Phase 4 enhancements: WebSocket auth, HTTPS UI, plugin hooks, build fixes
Security:
- Add token-based WebSocket authentication (closes critical security gap)
  - In-memory token store with 24h TTL (token_store.py)
  - POST /api/auth/token exchanges Basic Auth for WS token
  - GET /api/auth/status public endpoint for auth check
  - WebSocket validates token query param, rejects with close code 4401
  - Frontend LoginPrompt modal for credential entry
  - WebSocket manager handles full auth flow with auth_required state
  - No-op when AUTH_ENABLED=false (preserves existing behavior)

HTTPS:
- Wire HTTPS toggle in Settings UI (POST /api/system/ssl/toggle)
- Add certificate regeneration button
- Display SSL info (expiration, SANs, SHA256 fingerprint)

Plugins:
- Wire trigger_hook("pm3_command") in PM3 service
- Wire trigger_hook("update_check") in update manager

Build/Infrastructure:
- Enable NetworkManager in pi-gen AP setup stage
- Add HF booster board detection patch for Proxmark3
- Update LED PWM control patch
- Fix BLE adapter, UPS drivers, WiFi manager improvements
- Update HTTPS support stage script

Documentation:
- Update PROJECT_STATUS.md and IMPLEMENTATION_PRIORITIES.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 11:45:11 -08:00

89 lines
2.5 KiB
Python

"""Authentication module for Dangerous Pi API."""
import secrets
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from .. import config
from .token_store import create_token
security = HTTPBasic()
router = APIRouter()
def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)) -> str:
"""Verify HTTP Basic Auth credentials.
Args:
credentials: HTTP Basic credentials from request
Returns:
Username if authentication successful
Raises:
HTTPException: If authentication fails
"""
if not config.AUTH_ENABLED:
return "anonymous"
if not config.AUTH_PASSWORD:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="AUTH_ENABLED is true but AUTH_PASSWORD is not set",
)
# Use constant-time comparison to prevent timing attacks
is_correct_username = secrets.compare_digest(
credentials.username.encode("utf-8"),
config.AUTH_USERNAME.encode("utf-8")
)
is_correct_password = secrets.compare_digest(
credentials.password.encode("utf-8"),
config.AUTH_PASSWORD.encode("utf-8")
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
def get_optional_auth(credentials: HTTPBasicCredentials = Depends(security)) -> str | None:
"""Optional authentication - returns username or None.
Use this for endpoints where auth is optional based on config.
"""
if not config.AUTH_ENABLED:
return None
try:
return verify_credentials(credentials)
except HTTPException:
return None
@router.get("/status")
async def auth_status():
"""Check whether authentication is enabled.
This endpoint is always public so the frontend can determine
whether to prompt for credentials before connecting the WebSocket.
"""
return {"auth_enabled": config.AUTH_ENABLED}
@router.post("/token")
async def get_auth_token(username: str = Depends(verify_credentials)):
"""Exchange Basic Auth credentials for a WebSocket auth token.
The returned token should be passed as a query parameter when
connecting to the WebSocket endpoint: ws://host/ws/events?token=...
Tokens expire after 24 hours.
"""
token = create_token(username)
return {"token": token}