· TOTP· 2FA· pyotp· Python· FastAPI· sikkerhed· autentifikation· QR-kode· Google Authenticator· backup-koder
TOTP 2FA i Python med pyotp — tidsbaseret engangskoder til SaaS
TOTP (Time-based One-Time Password) 2FA med pyotp: hemmeligt nøgle-generering, QR-kode til authenticator-apps, verifikation, backup-koder og FastAPI integration.
Af M-Bus Gateway
M-Bus Gateway platformen tilbyder TOTP 2FA til udlejer- og admin-konti. Her er den komplette implementering med pyotp, QR-kode og backup-koder.
TOTP: Tidsbaseret engangskode
TOTP (Time-based One-Time Password) — RFC 6238:
→ Genererer 6-cifret kode baseret på:
- Delt hemmeligt nøgle (Base32-kodet)
- Nuværende tid (30-sekunders vindue)
→ Bruger HMAC-SHA1 (standard)
→ Samme nøgle → same kode i 30 sek → ny kode
Eksempel:
Nøgle: JBSWY3DPEHPK3PXP
Tid: 2026-05-24 10:00:15 UTC
Kode: 283746
30 sek senere (10:00:45):
Kode: 519284 (ny kode)
Sikkerhed:
→ Nøglen gemmes krypteret i databasen
→ Koden kan ikke genbruges (server markerer brugte koder)
→ Time-drift tolerance: ±30 sek (1 periode)
→ Brute force: 10^6 kombinationer / 30 sek = håbløst
pyotp: Implementering
# server/src/auth/totp.py
import pyotp
import qrcode
import io
import base64
import secrets
from server.src.core.config import settings
def generate_totp_secret() -> str:
"""
Generér ny TOTP hemmelighed.
32 bytes = 256 bits entropi (overkill men fremtidssikret).
"""
return pyotp.random_base32()
def get_totp_uri(secret: str, email: str) -> str:
"""
Generér otpauth:// URI til QR-kode.
Format: otpauth://totp/ISSUER:EMAIL?secret=SECRET&issuer=ISSUER
"""
totp = pyotp.TOTP(secret)
return totp.provisioning_uri(
name=email,
issuer_name="M-Bus Gateway",
)
def generate_qr_code_base64(uri: str) -> str:
"""
Generér QR-kode som base64-kodet PNG.
Sendes til frontend som data:image/png;base64,...
"""
img = qrcode.make(uri)
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return base64.b64encode(buf.read()).decode()
def verify_totp(secret: str, code: str) -> bool:
"""
Verificér TOTP-kode.
valid_window=1: Acceptér ±30 sekunder (1 ekstra vindue).
"""
if not code or not code.strip().isdigit() or len(code.strip()) != 6:
return False
totp = pyotp.TOTP(secret)
return totp.verify(code.strip(), valid_window=1)
Backup-koder: Nødadgang
# server/src/auth/backup_codes.py
import secrets
import hashlib
def generate_backup_codes(count: int = 10) -> tuple[list[str], list[str]]:
"""
Generér backup-koder til 2FA nødadgang.
Returnerer (plaintext_codes, hashed_codes).
Gem KUN hashed i DB — send plaintext til bruger én gang.
"""
codes = []
hashes = []
for _ in range(count):
# Format: XXXX-XXXX-XXXX (12 hex chars + bindestreger)
raw = secrets.token_hex(6) # 6 bytes = 12 hex chars
code = f"{raw[:4]}-{raw[4:8]}-{raw[8:]}".upper()
codes.append(code)
# Hash med SHA-256 (ingen salt nødvendigt — kode er allerede tilfældig):
code_hash = hashlib.sha256(code.encode()).hexdigest()
hashes.append(code_hash)
return codes, hashes
def verify_backup_code(code: str, stored_hashes: list[str]) -> tuple[bool, str | None]:
"""
Verificér backup-kode og returnér den brugte hash (til sletning).
Backup-koder kan kun bruges én gang.
"""
code = code.strip().upper().replace(" ", "")
code_hash = hashlib.sha256(code.encode()).hexdigest()
if code_hash in stored_hashes:
return True, code_hash
return False, None
FastAPI endpoints: 2FA setup flow
# server/src/auth/router.py
@router.post("/2fa/setup")
async def setup_2fa(
session: AsyncSession = Depends(get_session),
user: TokenPayload = Depends(get_current_user),
):
"""
Trin 1: Generér TOTP hemmelighed og QR-kode.
Bruger scanner QR → trin 2 verificerer.
"""
db_user = await session.get(User, user.sub)
if db_user.totp_enabled:
raise HTTPException(400, "2FA er allerede aktiveret")
# Generér nyt secret (gemmes IKKE i DB endnu — først ved verifikation):
secret = generate_totp_secret()
# Gem midlertidigt i Redis (5 min timeout):
await redis.setex(f"2fa_setup:{user.sub}", 300, secret)
qr_uri = get_totp_uri(secret, db_user.email)
qr_base64 = generate_qr_code_base64(qr_uri)
return {
"qr_code": f"data:image/png;base64,{qr_base64}",
"manual_key": secret, # Til manuel indtastning
}
@router.post("/2fa/verify-setup")
async def verify_2fa_setup(
body: TOTPVerifyIn,
session: AsyncSession = Depends(get_session),
user: TokenPayload = Depends(get_current_user),
):
"""
Trin 2: Verificér TOTP-kode fra authenticator-app.
Aktiverer 2FA og returnerer backup-koder.
"""
# Hent det midlertidigt gemte secret:
secret = await redis.get(f"2fa_setup:{user.sub}")
if not secret:
raise HTTPException(400, "Setup-session udløbet — start forfra")
secret = secret.decode()
if not verify_totp(secret, body.code):
raise HTTPException(400, "Forkert kode — prøv igen")
# Generér backup-koder:
plaintext_codes, hashed_codes = generate_backup_codes(10)
# Gem i DB:
db_user = await session.get(User, user.sub)
db_user.totp_secret = encrypt(secret) # Krypteret lagring
db_user.totp_backup_codes = hashed_codes
db_user.totp_enabled = True
session.add(db_user)
await session.commit()
# Slet Redis-nøgle:
await redis.delete(f"2fa_setup:{user.sub}")
return {
"status": "enabled",
"backup_codes": plaintext_codes, # Vis KUN én gang!
"message": "Gem backup-koderne på et sikkert sted",
}
@router.post("/2fa/verify")
async def verify_2fa_login(
body: TOTPVerifyIn,
session: AsyncSession = Depends(get_session),
user_id: str = Depends(get_pending_2fa_user), # Fra 2FA-session cookie
):
"""Verificér TOTP-kode ved login — fuldfør autentifikation."""
db_user = await session.get(User, user_id)
if not db_user.totp_enabled:
raise HTTPException(400, "2FA ikke aktiveret")
# Prøv TOTP-kode:
secret = decrypt(db_user.totp_secret)
if verify_totp(secret, body.code):
return await _issue_full_tokens(db_user)
# Prøv backup-kode:
valid, used_hash = verify_backup_code(body.code, db_user.totp_backup_codes)
if valid:
# Slet brugt backup-kode:
db_user.totp_backup_codes = [h for h in db_user.totp_backup_codes if h != used_hash]
session.add(db_user)
await session.commit()
return await _issue_full_tokens(db_user)
raise HTTPException(400, "Forkert kode")
Konklusion
TOTP 2FA med pyotp giver stærk second-factor autentifikation uden ekstern afhængighed. QR-kode til Google Authenticator/Authy. Backup-koder med SHA-256-hashing og single-use sikrer nødadgang. FastAPI integration: 2-trins setup (generér + verificér), 2FA-login som sekundær check efter primær adgangskode-login.