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:
@@ -1,8 +1,9 @@
|
||||
"""Security utilities — password hashing (bcrypt) and token encryption (Fernet)."""
|
||||
"""Security utilities — password hashing (bcrypt), token encryption (Fernet), TOTP."""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
|
||||
import pyotp
|
||||
from cryptography.fernet import Fernet
|
||||
from passlib.context import CryptContext
|
||||
|
||||
@@ -91,6 +92,32 @@ def generate_relay_secret() -> str:
|
||||
return secrets.token_hex(16)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TOTP (Time-based One-Time Password)
|
||||
# ---------------------------------------------------------------------------
|
||||
def generate_totp_secret() -> str:
|
||||
"""Generate a new random TOTP secret (base32-encoded)."""
|
||||
return pyotp.random_base32()
|
||||
|
||||
|
||||
def verify_totp(secret: str, code: str) -> bool:
|
||||
"""Verify a 6-digit TOTP code against a secret.
|
||||
|
||||
Allows a window of +/- 1 interval (30s) to account for clock drift.
|
||||
"""
|
||||
totp = pyotp.TOTP(secret)
|
||||
return totp.verify(code, valid_window=1)
|
||||
|
||||
|
||||
def generate_totp_uri(secret: str, username: str, issuer: str = "NetBird MSP") -> str:
|
||||
"""Generate an otpauth:// URI for QR code generation."""
|
||||
totp = pyotp.TOTP(secret)
|
||||
return totp.provisioning_uri(name=username, issuer_name=issuer)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Misc key generation
|
||||
# ---------------------------------------------------------------------------
|
||||
def generate_datastore_encryption_key() -> str:
|
||||
"""Generate a base64-encoded 32-byte key for NetBird DataStoreEncryptionKey.
|
||||
|
||||
|
||||
@@ -23,6 +23,19 @@ class ChangePasswordRequest(BaseModel):
|
||||
new_password: str = Field(..., min_length=12, max_length=128)
|
||||
|
||||
|
||||
class MfaTokenRequest(BaseModel):
|
||||
"""Request containing only an MFA token (for setup initiation)."""
|
||||
|
||||
mfa_token: str = Field(..., min_length=1)
|
||||
|
||||
|
||||
class MfaVerifyRequest(BaseModel):
|
||||
"""MFA TOTP verification payload."""
|
||||
|
||||
mfa_token: str = Field(..., min_length=1)
|
||||
totp_code: str = Field(..., min_length=6, max_length=6)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Customer
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -113,6 +126,7 @@ class SystemConfigUpdate(BaseModel):
|
||||
branding_name: Optional[str] = Field(None, max_length=255)
|
||||
branding_subtitle: Optional[str] = Field(None, max_length=255)
|
||||
default_language: Optional[str] = Field(None, max_length=10)
|
||||
mfa_enabled: Optional[bool] = None
|
||||
azure_enabled: Optional[bool] = None
|
||||
azure_tenant_id: Optional[str] = Field(None, max_length=255)
|
||||
azure_client_id: Optional[str] = Field(None, max_length=255)
|
||||
|
||||
Reference in New Issue
Block a user