Arag / app /services /totp_setup_service.py
AuthorBot
Add mandatory SuperAdmin TOTP enrollment and production delivery skills.
19a1415
Raw
History Blame Contribute Delete
3.45 kB
"""SuperAdmin TOTP enrollment β€” mandatory before privileged operations."""
from __future__ import annotations
import structlog
from fastapi import HTTPException
from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.access.totp import generate_qr_code_base64, generate_totp_secret, verify_totp
from app.core.crypto.credential_crypto import decrypt_secret, encrypt_secret
from app.models.user import User
from app.repositories.audit_repo import AuditRepository
from app.repositories.user_repo import UserRepository
logger = structlog.get_logger(__name__)
_PENDING_PREFIX = "totp_pending:v1:"
_PENDING_TTL = 600
class TotpSetupService:
"""Manage SuperAdmin two-factor enrollment lifecycle."""
def __init__(self, db: AsyncSession, redis: Redis | None = None) -> None:
self._db = db
self._redis = redis
self._users = UserRepository(db)
self._audit = AuditRepository(db)
async def status(self, actor: User) -> dict:
"""Return whether TOTP is configured for the current SuperAdmin."""
return {"configured": bool(actor.totp_secret)}
async def begin_setup(self, actor: User) -> dict:
"""Start enrollment β€” returns QR for authenticator app."""
if actor.totp_secret:
raise HTTPException(400, "TOTP already configured for this account")
secret = generate_totp_secret()
await self._store_pending(actor.id, secret)
logger.info("TOTP enrollment started", actor_email=actor.email)
return {
"qr_code_data_uri": generate_qr_code_base64(secret, actor.email),
"manual_entry_key": secret,
}
async def confirm_setup(self, actor: User, code: str) -> dict:
"""Verify authenticator code and persist TOTP secret."""
if actor.totp_secret:
raise HTTPException(400, "TOTP already configured for this account")
pending = await self._load_pending(actor.id)
if not pending:
raise HTTPException(400, "Enrollment expired β€” scan QR again")
if not verify_totp(pending, code):
raise HTTPException(400, "Invalid code β€” check your authenticator app")
await self._users.set_totp_secret(actor.id, pending)
await self._clear_pending(actor.id)
await self._audit.log(
actor_id=actor.id,
actor_email=actor.email,
action="totp_enrolled",
target_type="user",
target_id=actor.id,
details={},
)
await self._db.flush()
logger.info("TOTP enrollment completed", actor_email=actor.email)
return {"configured": True}
async def _store_pending(self, user_id: str, secret: str) -> None:
if self._redis is None:
raise HTTPException(503, "Redis required for TOTP enrollment")
await self._redis.set(
_PENDING_PREFIX + user_id,
encrypt_secret(secret),
ex=_PENDING_TTL,
)
async def _load_pending(self, user_id: str) -> str | None:
if self._redis is None:
return None
raw = await self._redis.get(_PENDING_PREFIX + user_id)
if not raw:
return None
return decrypt_secret(raw)
async def _clear_pending(self, user_id: str) -> None:
if self._redis is None:
return
await self._redis.delete(_PENDING_PREFIX + user_id)