Auth: Nur noch Magic Link, Code-Verifizierung entfernt

- /api/auth/verify-code Endpoint entfernt
- generate_magic_code() und VerifyCodeRequest entfernt
- VerifyCodeLimiter (Brute-Force-Schutz) entfernt (nicht mehr noetig)
- E-Mail-Template: Nur noch Anmelde-Link, kein 6-stelliger Code
- Login-Seite: Zeigt nach E-Mail-Eingabe Hinweis statt Code-Feld
- Magic Link Token-Verifikation via URL bleibt bestehen
Dieser Commit ist enthalten in:
Claude Dev
2026-03-25 00:01:19 +01:00
Ursprung 5789cc1706
Commit 8f1a45c1a9
6 geänderte Dateien mit 35 neuen und 213 gelöschten Zeilen

Datei anzeigen

@@ -6,7 +6,6 @@ from models import (
MagicLinkRequest,
MagicLinkResponse,
VerifyTokenRequest,
VerifyCodeRequest,
TokenResponse,
UserMeResponse,
)
@@ -14,13 +13,12 @@ from auth import (
create_token,
get_current_user,
generate_magic_token,
generate_magic_code,
)
from database import db_dependency
from config import TIMEZONE, MAGIC_LINK_EXPIRE_MINUTES, MAGIC_LINK_BASE_URL
from email_utils.sender import send_email
from email_utils.templates import magic_link_login_email
from email_utils.rate_limiter import magic_link_limiter, verify_code_limiter
from email_utils.rate_limiter import magic_link_limiter
import aiosqlite
logger = logging.getLogger("osint.auth")
@@ -34,7 +32,7 @@ async def request_magic_link(
request: Request,
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Magic Link anfordern. Sendet E-Mail mit Link + Code."""
"""Magic Link anfordern. Sendet E-Mail mit Link."""
email = data.email.lower().strip()
ip = request.client.host if request.client else "unknown"
@@ -42,7 +40,6 @@ async def request_magic_link(
allowed, reason = magic_link_limiter.check(email, ip)
if not allowed:
logger.warning(f"Rate-Limit fuer {email} von {ip}: {reason}")
# Trotzdem 200 zurueckgeben (kein Information-Leak)
return MagicLinkResponse(message="Wenn ein Konto existiert, wurde eine E-Mail gesendet.")
# Nutzer suchen
@@ -75,9 +72,8 @@ async def request_magic_link(
magic_link_limiter.record(email, ip)
return MagicLinkResponse(message="Wenn ein Konto existiert, wurde eine E-Mail gesendet.")
# Token + Code generieren
# Token generieren
token = generate_magic_token()
code = generate_magic_code()
expires_at = (datetime.now(TIMEZONE) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES)).strftime('%Y-%m-%d %H:%M:%S')
# Alte ungenutzte Magic Links fuer diese E-Mail invalidieren
@@ -89,14 +85,14 @@ async def request_magic_link(
# Neuen Magic Link speichern
await db.execute(
"""INSERT INTO magic_links (email, token, code, purpose, user_id, expires_at, ip_address)
VALUES (?, ?, ?, 'login', ?, ?, ?)""",
(email, token, code, user["id"], expires_at, ip),
VALUES (?, ?, '', 'login', ?, ?, ?)""",
(email, token, user["id"], expires_at, ip),
)
await db.commit()
# E-Mail senden
link = f"{MAGIC_LINK_BASE_URL}/?token={token}"
subject, html = magic_link_login_email(user["email"].split("@")[0], code, link)
subject, html = magic_link_login_email(user["email"].split("@")[0], link)
await send_email(email, subject, html)
magic_link_limiter.record(email, ip)
@@ -160,84 +156,6 @@ async def verify_magic_link(
)
@router.post("/verify-code", response_model=TokenResponse)
async def verify_magic_code(
data: VerifyCodeRequest,
request: Request,
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Magic Code verifizieren (6-stelliger Code + E-Mail)."""
email = data.email.lower().strip()
ip = request.client.host if request.client else "unknown"
# Brute-Force-Schutz: Fehlversuche pruefen
allowed, reason = verify_code_limiter.check(email, ip)
if not allowed:
logger.warning(f"Verify-Code Rate-Limit fuer {email} von {ip}: {reason}")
# Bei Sperre alle offenen Magic Links fuer diese E-Mail invalidieren
await db.execute(
"UPDATE magic_links SET is_used = 1 WHERE email = ? AND is_used = 0",
(email,),
)
await db.commit()
raise HTTPException(status_code=429, detail=reason)
cursor = await db.execute(
"""SELECT ml.*, u.username, u.email as user_email, u.role, u.organization_id, u.is_active,
o.slug as org_slug, o.is_active as org_active
FROM magic_links ml
JOIN users u ON u.id = ml.user_id
JOIN organizations o ON o.id = u.organization_id
WHERE LOWER(ml.email) = ? AND ml.code = ? AND ml.is_used = 0
ORDER BY ml.created_at DESC LIMIT 1""",
(email, data.code),
)
ml = await cursor.fetchone()
if not ml:
verify_code_limiter.record_failure(email, ip)
logger.warning(f"Fehlgeschlagener Code-Versuch fuer {email} von {ip}")
raise HTTPException(status_code=400, detail="Ungueltiger Code")
# Ablauf pruefen
now = datetime.now(TIMEZONE)
expires = datetime.fromisoformat(ml["expires_at"])
if expires.tzinfo is None:
expires = expires.replace(tzinfo=TIMEZONE)
if now > expires:
raise HTTPException(status_code=400, detail="Code abgelaufen. Bitte neuen Code anfordern.")
if not ml["is_active"] or not ml["org_active"]:
raise HTTPException(status_code=403, detail="Konto oder Organisation deaktiviert")
# Magic Link als verwendet markieren
await db.execute("UPDATE magic_links SET is_used = 1 WHERE id = ?", (ml["id"],))
# Letzten Login aktualisieren
await db.execute(
"UPDATE users SET last_login_at = ? WHERE id = ?",
(now.isoformat(), ml["user_id"]),
)
await db.commit()
# Fehlversuche-Zaehler nach Erfolg zuruecksetzen
verify_code_limiter.clear(email)
token = create_token(
user_id=ml["user_id"],
username=ml["username"],
email=ml["user_email"],
role=ml["role"],
tenant_id=ml["organization_id"],
org_slug=ml["org_slug"],
)
return TokenResponse(
access_token=token,
username=ml["username"],
)
@router.get("/me", response_model=UserMeResponse)
async def get_me(
current_user: dict = Depends(get_current_user),