Add TOTP-based Multi-Factor Authentication (MFA) for local users

Global MFA toggle in Security settings, QR code setup on first login,
6-digit TOTP verification on subsequent logins. Azure AD users exempt.
Admins can reset user MFA. TOTP secrets encrypted at rest with Fernet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 23:14:06 +01:00
parent 647630ff19
commit 3d28f13054
13 changed files with 615 additions and 62 deletions

View File

@@ -30,6 +30,39 @@ def create_access_token(username: str, expires_delta: Optional[timedelta] = None
return jwt.encode(payload, SECRET_KEY, algorithm=JWT_ALGORITHM)
def create_mfa_token(username: str) -> str:
"""Create a short-lived JWT for the MFA verification step (5 min)."""
expire = datetime.utcnow() + timedelta(minutes=5)
payload = {"sub": username, "exp": expire, "purpose": "mfa"}
return jwt.encode(payload, SECRET_KEY, algorithm=JWT_ALGORITHM)
def verify_mfa_token(token: str) -> str:
"""Verify an MFA-purpose JWT and return the username.
Raises HTTPException if the token is invalid, expired, or not an MFA token.
"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[JWT_ALGORITHM])
if payload.get("purpose") != "mfa":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid MFA token.",
)
username: Optional[str] = payload.get("sub")
if not username:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid MFA token.",
)
return username
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="MFA token expired or invalid.",
)
def get_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_scheme),
db: Session = Depends(get_db),