Some checks failed
Lint & Test / test (push) Failing after 29s
Auth is now optional: when `auth.api_keys` is empty, all endpoints are open (no login screen, no Bearer tokens). Health endpoint reports `auth_required` so the frontend knows which mode to use. Backup/restore fixes: - Auto-backup uses atomic writes (was `write_text`, risked corruption) - Startup backup skipped if recent backup exists (<5 min cooldown), preventing rapid restarts from rotating out good backups - Restore rejects all-empty backups to prevent accidental data wipes - Store saves frozen after restore to prevent stale in-memory data from overwriting freshly-restored files before restart completes - Missing stores during restore logged as warnings - STORE_MAP completeness verified at startup against StorageConfig
99 lines
2.9 KiB
Python
99 lines
2.9 KiB
Python
"""Authentication module for API key validation."""
|
|
|
|
import secrets
|
|
from typing import Annotated
|
|
|
|
from fastapi import Depends, HTTPException, Security, status
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
|
|
from wled_controller.config import get_config
|
|
from wled_controller.utils import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
# Security scheme for Bearer token
|
|
security = HTTPBearer(auto_error=False)
|
|
|
|
|
|
def is_auth_enabled() -> bool:
|
|
"""Return True when at least one API key is configured."""
|
|
return bool(get_config().auth.api_keys)
|
|
|
|
|
|
def verify_api_key(
|
|
credentials: Annotated[HTTPAuthorizationCredentials | None, Security(security)]
|
|
) -> str:
|
|
"""Verify API key from Authorization header.
|
|
|
|
When no API keys are configured, authentication is disabled and all
|
|
requests are allowed through as "anonymous".
|
|
|
|
Args:
|
|
credentials: HTTP authorization credentials
|
|
|
|
Returns:
|
|
Label/identifier of the authenticated client
|
|
|
|
Raises:
|
|
HTTPException: If authentication is required but invalid
|
|
"""
|
|
config = get_config()
|
|
|
|
# No keys configured → auth disabled, allow all requests
|
|
if not config.auth.api_keys:
|
|
return "anonymous"
|
|
|
|
# Check if credentials are provided
|
|
if not credentials:
|
|
logger.warning("Request missing Authorization header")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Missing API key - authentication is required",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# Extract token
|
|
token = credentials.credentials
|
|
|
|
# Find matching key and return its label using constant-time comparison
|
|
authenticated_as = None
|
|
for label, api_key in config.auth.api_keys.items():
|
|
if secrets.compare_digest(token, api_key):
|
|
authenticated_as = label
|
|
break
|
|
|
|
if not authenticated_as:
|
|
logger.warning("Invalid API key attempt")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid API key",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# Log successful authentication
|
|
logger.debug(f"Authenticated as: {authenticated_as}")
|
|
|
|
return authenticated_as
|
|
|
|
|
|
# Dependency for protected routes
|
|
# Returns the label/identifier of the authenticated client
|
|
AuthRequired = Annotated[str, Depends(verify_api_key)]
|
|
|
|
|
|
def verify_ws_token(token: str) -> bool:
|
|
"""Check a WebSocket query-param token against configured API keys.
|
|
|
|
When no API keys are configured, authentication is disabled and all
|
|
WebSocket connections are allowed.
|
|
"""
|
|
config = get_config()
|
|
# No keys configured → auth disabled, allow all connections
|
|
if not config.auth.api_keys:
|
|
return True
|
|
if token:
|
|
for _label, api_key in config.auth.api_keys.items():
|
|
if secrets.compare_digest(token, api_key):
|
|
return True
|
|
return False
|