- Add POST /api/auth/change-username (password + OTP when 2FA is on, format/reserved-name checks, uniqueness) and a Change Username modal in Account.vue; use the real username for the 2FA provisioning URI. - Stop creating the built-in admin/password user; the initial admin is seeded only from OVPMON_INITIAL_ADMIN_USER/PASSWORD. - Reject 2FA-pending temporary tokens on all protected routes (Flask token_required, Profiler verify_token); only /api/auth/verify-2fa accepts them. - Stop logging the OTP and TOTP secret in enable_2fa. - Profiler: validate profile username (pattern + realpath checks in schema, router, pki and generator) to prevent path traversal. - Restrict CORS to the panel origin in Profiler and Flask APIs. - UI: header username no longer sticks to the hardcoded Admin fallback; it is synced from /user/me and updated after a rename. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
import jwt
|
|
import configparser
|
|
import os
|
|
from fastapi import Header, HTTPException, status
|
|
from pathlib import Path
|
|
|
|
from .config import get_config_value
|
|
|
|
def get_secret_key():
|
|
# Use consistent OVPMON_API_SECRET_KEY as primary source
|
|
key = get_config_value('api', 'secret_key', fallback='ovpmon-secret-change-me')
|
|
|
|
if key == 'ovpmon-secret-change-me':
|
|
print("[AUTH] WARNING: Using default fallback SECRET_KEY")
|
|
else:
|
|
# Check if it was from env (get_config_value prioritizes env)
|
|
import os
|
|
if os.getenv('OVPMON_API_SECRET_KEY'):
|
|
print("[AUTH] Using SECRET_KEY from OVPMON_API_SECRET_KEY environment variable")
|
|
elif os.getenv('OVPMON_SECRET_KEY'):
|
|
print("[AUTH] Using SECRET_KEY from OVPMON_SECRET_KEY environment variable")
|
|
else:
|
|
print("[AUTH] SECRET_KEY loaded (config.ini or fallback)")
|
|
|
|
return key
|
|
|
|
SECRET_KEY = get_secret_key()
|
|
|
|
|
|
async def verify_token(authorization: str = Header(None)):
|
|
if not authorization or not authorization.startswith("Bearer "):
|
|
print(f"[AUTH] Missing or invalid Authorization header: {authorization[:20] if authorization else 'None'}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token is missing or invalid",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
token = authorization.split(" ")[1]
|
|
|
|
try:
|
|
# Debug: Log a few chars of the key and token (safely)
|
|
# print(f"[AUTH] Decoding token with SECRET_KEY starting with: {SECRET_KEY[:3]}...")
|
|
|
|
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
|
if payload.get("is_2fa_pending"):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="2FA verification required",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
return payload
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
error_type = type(e).__name__
|
|
error_detail = str(e)
|
|
print(f"[AUTH] JWT Decode Failed. Type: {error_type}, Detail: {error_detail}")
|
|
|
|
# Handling exceptions dynamically to avoid AttributeError
|
|
if error_type == "ExpiredSignatureError":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token has expired",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
elif error_type in ["InvalidTokenError", "DecodeError"]:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token is invalid",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
else:
|
|
# Check if it's a TypeError (e.g. wrong arguments for decode)
|
|
if error_type == "TypeError":
|
|
print("[AUTH] Critical: jwt.decode failed with TypeError. This likely means 'jwt' package is installed instead of 'PyJWT'.")
|
|
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=f"Authentication error: {error_type}",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|