feat: Implement TOTP-based Two-Factor Authentication

- Add django-otp and qrcode dependencies
- Create comprehensive 2FA views and templates in German
- Add 2FA setup, verification, and management interfaces
- Implement backup token system with 10 recovery codes
- Add TwoFactorMiddleware for session enforcement
- Integrate 2FA controls into user navigation menu
- Support QR code generation for authenticator apps
- Add forms for secure 2FA operations with validation
- Configure OTP settings and admin site integration

Features:
- Optional 2FA (users can enable/disable)
- TOTP compatible with Google Authenticator, Authy, etc.
- Backup codes for emergency access
- German language interface
- Session-based 2FA enforcement
- Password confirmation for sensitive operations
- Production-ready with HTTPS support
This commit is contained in:
2025-09-30 00:10:02 +02:00
parent 92b689f5e7
commit ed6a02232e
29 changed files with 41444 additions and 1 deletions

View File

@@ -622,6 +622,12 @@
<li><a class="dropdown-item" href="{% url 'stiftung:user_detail' user.pk %}">
<i class="fas fa-user me-2"></i>Mein Profil
</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Sicherheit</h6></li>
<li><a class="dropdown-item" href="{% url 'stiftung:two_factor_setup' %}">
<i class="fas fa-shield-alt me-2"></i>2FA verwalten
</a></li>
<li><hr class="dropdown-divider"></li>
{% if perms.stiftung.manage_users %}
<li><a class="dropdown-item" href="{% url 'stiftung:user_management' %}">
<i class="fas fa-users me-2"></i>Benutzerverwaltung

View File

@@ -0,0 +1,157 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-key text-success"></i>
Backup-Codes für Zwei-Faktor-Authentifizierung
</h4>
</div>
<div class="card-body">
<div class="alert alert-success">
<h5><i class="fas fa-check-circle"></i> Zwei-Faktor-Authentifizierung aktiviert!</h5>
<p class="mb-0">
Ihr Konto ist jetzt mit Zwei-Faktor-Authentifizierung geschützt.
</p>
</div>
<div class="alert alert-warning">
<h6><i class="fas fa-exclamation-triangle"></i> Wichtig: Backup-Codes sicher speichern</h6>
<p class="mb-0">
Diese Backup-Codes können Sie verwenden, falls Sie keinen Zugriff auf Ihre
Authenticator-App haben. <strong>Speichern Sie diese Codes an einem sicheren Ort!</strong>
</p>
</div>
<div class="row">
<div class="col-md-6">
<h5>Ihre Backup-Codes:</h5>
<div class="bg-light p-3 rounded mb-3" id="backup-codes">
{% for token in backup_tokens %}
<div class="font-monospace fw-bold mb-2">{{ token }}</div>
{% endfor %}
</div>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-primary" onclick="copyBackupCodes()">
<i class="fas fa-copy"></i>
Codes kopieren
</button>
<button type="button" class="btn btn-outline-secondary" onclick="printBackupCodes()">
<i class="fas fa-print"></i>
Codes drucken
</button>
</div>
</div>
<div class="col-md-6">
<h5>Verwendung der Backup-Codes:</h5>
<ul class="text-muted">
<li>Jeder Code kann nur <strong>einmal</strong> verwendet werden</li>
<li>Geben Sie den Code anstelle des Authenticator-Codes ein</li>
<li>Codes sind 8 Zeichen lang (Buchstaben und Zahlen)</li>
<li>Bewahren Sie die Codes sicher und vertraulich auf</li>
</ul>
<div class="alert alert-info small">
<h6><i class="fas fa-lightbulb"></i> Empfohlene Aufbewahrung:</h6>
<ul class="mb-0 small">
<li>In einem Passwort-Manager</li>
<li>Ausgedruckt in einem Tresor</li>
<li>Verschlüsselt auf einem sicheren Gerät</li>
</ul>
</div>
</div>
</div>
<div class="text-center mt-4">
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-primary btn-lg">
<i class="fas fa-home"></i>
Weiter zum Dashboard
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function copyBackupCodes() {
const codes = [
{% for token in backup_tokens %}'{{ token }}'{% if not forloop.last %},{% endif %}{% endfor %}
];
const text = codes.join('\n');
navigator.clipboard.writeText(text).then(function() {
// Show success message
const btn = event.target.closest('button');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check text-success"></i> Kopiert!';
btn.classList.add('btn-success');
btn.classList.remove('btn-outline-primary');
setTimeout(function() {
btn.innerHTML = originalText;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-primary');
}, 2000);
}).catch(function(err) {
alert('Fehler beim Kopieren: ' + err);
});
}
function printBackupCodes() {
const codes = [
{% for token in backup_tokens %}'{{ token }}'{% if not forloop.last %},{% endif %}{% endfor %}
];
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<html>
<head>
<title>Backup-Codes - Stiftung Management System</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; }
.codes { background: #f8f9fa; padding: 20px; border-radius: 5px; }
.code { font-family: monospace; font-size: 14px; margin: 10px 0; font-weight: bold; }
.warning { color: #d63384; font-weight: bold; margin: 20px 0; }
.date { color: #666; font-size: 12px; }
</style>
</head>
<body>
<h1>Backup-Codes für Zwei-Faktor-Authentifizierung</h1>
<p><strong>Konto:</strong> ${window.location.hostname}</p>
<p class="date"><strong>Generiert am:</strong> ${new Date().toLocaleString('de-DE')}</p>
<div class="warning">
⚠️ WICHTIG: Diese Codes sind vertraulich und können nur einmal verwendet werden!
</div>
<div class="codes">
${codes.map(code => '<div class="code">' + code + '</div>').join('')}
</div>
<p><strong>Verwendung:</strong></p>
<ul>
<li>Verwenden Sie diese Codes, falls Sie keinen Zugriff auf Ihre Authenticator-App haben</li>
<li>Geben Sie einen Code anstelle des Authenticator-Codes ein</li>
<li>Jeder Code kann nur einmal verwendet werden</li>
<li>Bewahren Sie diese Codes sicher auf</li>
</ul>
</body>
</html>
`);
printWindow.document.close();
printWindow.print();
}
</script>
{% endblock %}

View File

@@ -0,0 +1,146 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-key text-primary"></i>
Backup-Codes verwalten
</h4>
</div>
<div class="card-body">
{% if has_tokens %}
<div class="alert alert-info">
<h6><i class="fas fa-info-circle"></i> Backup-Codes Status</h6>
<p class="mb-0">
Sie haben derzeit <strong>{{ token_count }} Backup-Codes</strong> verfügbar.
Aus Sicherheitsgründen werden die Codes nicht angezeigt.
</p>
</div>
<div class="row">
<div class="col-md-6">
<h5>Neue Backup-Codes generieren</h5>
<p class="text-muted">
Wenn Sie neue Backup-Codes benötigen (z.B. weil Sie alle verbraucht haben
oder sie verloren haben), können Sie neue generieren.
</p>
<div class="alert alert-warning small">
<strong>Warnung:</strong> Das Generieren neuer Codes macht alle
bisherigen Backup-Codes ungültig.
</div>
</div>
<div class="col-md-6">
<h5>Passwort eingeben</h5>
<p class="text-muted">
Geben Sie Ihr Passwort ein, um neue Backup-Codes zu generieren:
</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="regenerate" value="1">
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password"
class="form-control"
id="password"
name="password"
required
autocomplete="current-password">
</div>
<div class="d-grid">
<button type="submit" class="btn btn-warning">
<i class="fas fa-sync-alt"></i>
Neue Backup-Codes generieren
</button>
</div>
</form>
</div>
</div>
{% else %}
<div class="alert alert-warning">
<h6><i class="fas fa-exclamation-triangle"></i> Keine Backup-Codes vorhanden</h6>
<p class="mb-0">
Sie haben derzeit keine Backup-Codes. Das kann passieren, wenn:
</p>
<ul class="mb-0 mt-2">
<li>Sie alle Codes bereits verwendet haben</li>
<li>Die Zwei-Faktor-Authentifizierung neu eingerichtet wurde</li>
<li>Die Codes aus anderen Gründen gelöscht wurden</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<h5>Neue Backup-Codes generieren</h5>
<p class="text-muted">
Generieren Sie neue Backup-Codes, um sicherzustellen, dass Sie
auch ohne Authenticator-App auf Ihr Konto zugreifen können.
</p>
</div>
<div class="col-md-6">
<h5>Passwort eingeben</h5>
<form method="post">
{% csrf_token %}
<input type="hidden" name="regenerate" value="1">
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password"
class="form-control"
id="password"
name="password"
required
autocomplete="current-password">
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-plus"></i>
Backup-Codes generieren
</button>
</div>
</form>
</div>
</div>
{% endif %}
<div class="row mt-4">
<div class="col-12">
<div class="alert alert-info">
<h6><i class="fas fa-lightbulb"></i> Über Backup-Codes:</h6>
<ul class="mb-0 small">
<li><strong>Zweck:</strong> Zugriff auf Ihr Konto, wenn die Authenticator-App nicht verfügbar ist</li>
<li><strong>Anzahl:</strong> 10 Codes werden generiert</li>
<li><strong>Verwendung:</strong> Jeder Code kann nur einmal verwendet werden</li>
<li><strong>Format:</strong> 8 Zeichen (Buchstaben und Zahlen)</li>
<li><strong>Sicherheit:</strong> Codes sollten sicher aufbewahrt werden</li>
</ul>
</div>
</div>
</div>
<div class="text-center mt-3">
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i>
Zurück zum Dashboard
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,84 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-shield-alt text-warning"></i>
Zwei-Faktor-Authentifizierung deaktivieren
</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
<h6><i class="fas fa-exclamation-triangle"></i> Warnung</h6>
<p class="mb-0">
Sie sind dabei, die Zwei-Faktor-Authentifizierung zu deaktivieren.
Dies verringert die Sicherheit Ihres Kontos erheblich.
</p>
</div>
<h5>Was wird deaktiviert:</h5>
<ul class="text-muted mb-4">
<li>Authenticator-App Codes</li>
<li>Alle bestehenden Backup-Codes</li>
<li>Zusätzliche Sicherheitsebene beim Login</li>
</ul>
<h5>Bestätigung erforderlich</h5>
<p class="text-muted">
Geben Sie Ihr Passwort ein, um die Zwei-Faktor-Authentifizierung
zu deaktivieren:
</p>
<form method="post">
{% csrf_token %}
<div class="mb-4">
<label for="password" class="form-label">Passwort</label>
<input type="password"
class="form-control"
id="password"
name="password"
required
autocomplete="current-password"
autofocus>
<div class="form-text">
Ihr aktuelles Konto-Passwort
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-warning btn-lg">
<i class="fas fa-times-circle"></i>
Zwei-Faktor-Authentifizierung deaktivieren
</button>
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i>
Abbrechen
</a>
</div>
</form>
<div class="alert alert-info mt-4">
<h6><i class="fas fa-lightbulb"></i> Alternative Empfehlung</h6>
<p class="mb-0 small">
Statt die 2FA zu deaktivieren, können Sie auch:
</p>
<ul class="mb-0 small mt-1">
<li>Neue Backup-Codes generieren, falls Sie den Zugang verloren haben</li>
<li>Die 2FA neu einrichten, falls Probleme mit der App bestehen</li>
<li>Den Administrator kontaktieren, wenn Sie Hilfe benötigen</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,124 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-shield-alt text-success"></i>
Zwei-Faktor-Authentifizierung verwalten
</h4>
</div>
<div class="card-body">
<div class="alert alert-success">
<h6><i class="fas fa-check-circle"></i> 2FA ist aktiviert</h6>
<p class="mb-0">
Ihr Konto ist mit Zwei-Faktor-Authentifizierung geschützt.
Sie benötigen bei jeder Anmeldung einen Code aus Ihrer Authenticator-App.
</p>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-key"></i>
Backup-Codes
</h6>
</div>
<div class="card-body">
<p class="text-muted small">
Backup-Codes ermöglichen den Zugriff auf Ihr Konto,
wenn Ihre Authenticator-App nicht verfügbar ist.
</p>
{% if backup_token_count > 0 %}
<p class="mb-2">
<strong>{{ backup_token_count }} Backup-Codes</strong> verfügbar
</p>
<div class="d-grid">
<a href="{% url 'stiftung:backup_tokens' %}" class="btn btn-outline-primary">
<i class="fas fa-eye"></i>
Codes verwalten
</a>
</div>
{% else %}
<div class="alert alert-warning small">
<strong>Keine Backup-Codes vorhanden!</strong><br>
Es wird empfohlen, Backup-Codes zu generieren.
</div>
<div class="d-grid">
<a href="{% url 'stiftung:backup_tokens' %}" class="btn btn-warning">
<i class="fas fa-plus"></i>
Backup-Codes generieren
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-cog"></i>
Einstellungen
</h6>
</div>
<div class="card-body">
<p class="text-muted small">
Verwalten Sie Ihre 2FA-Einstellungen oder
deaktivieren Sie die Zwei-Faktor-Authentifizierung.
</p>
<div class="d-grid gap-2">
<a href="{% url 'stiftung:two_factor_disable' %}" class="btn btn-outline-danger">
<i class="fas fa-times-circle"></i>
2FA deaktivieren
</a>
</div>
<div class="mt-3">
<small class="text-muted">
<strong>Gerät eingerichtet:</strong><br>
Standard TOTP-Device
</small>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="alert alert-info">
<h6><i class="fas fa-lightbulb"></i> Tipps für 2FA:</h6>
<ul class="mb-0 small">
<li><strong>Authenticator-Apps:</strong> Google Authenticator, Microsoft Authenticator, Authy</li>
<li><strong>Backup-Codes:</strong> Bewahren Sie diese sicher auf (z.B. Passwort-Manager)</li>
<li><strong>Sicherheit:</strong> 2FA schützt auch bei kompromittierten Passwörtern</li>
<li><strong>Neues Gerät:</strong> Bei Gerätewechsel 2FA deaktivieren und neu einrichten</li>
</ul>
</div>
</div>
</div>
<div class="text-center mt-3">
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i>
Zurück zum Dashboard
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,129 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-shield-alt text-primary"></i>
Zwei-Faktor-Authentifizierung einrichten
</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h5>Schritt 1: Authenticator App installieren</h5>
<p class="text-muted">
Installieren Sie eine Authenticator-App auf Ihrem Smartphone:
</p>
<ul class="mb-4">
<li><strong>Google Authenticator</strong> (iOS/Android)</li>
<li><strong>Microsoft Authenticator</strong> (iOS/Android)</li>
<li><strong>Authy</strong> (iOS/Android/Desktop)</li>
<li><strong>1Password</strong> (Premium)</li>
</ul>
<h5>Schritt 2: QR-Code scannen</h5>
<p class="text-muted">
Scannen Sie den QR-Code mit Ihrer Authenticator-App:
</p>
<div class="text-center mb-4">
<img src="{% url 'stiftung:two_factor_qr' %}"
alt="QR Code für 2FA Setup"
class="img-fluid border rounded"
style="max-width: 200px;">
</div>
<details class="mb-4">
<summary class="text-muted small">Manueller Setup-Code anzeigen</summary>
<div class="mt-2 p-2 bg-light rounded">
<small class="font-monospace">{{ device.key }}</small>
</div>
<small class="text-muted d-block mt-1">
Falls der QR-Code nicht funktioniert, können Sie diesen Code manuell eingeben.
</small>
</details>
</div>
<div class="col-md-6">
<h5>Schritt 3: Bestätigen</h5>
<p class="text-muted">
Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein:
</p>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="token" class="form-label">Bestätigungscode</label>
<input type="text"
class="form-control"
id="token"
name="token"
placeholder="000000"
maxlength="6"
pattern="[0-9]{6}"
required
autocomplete="off">
<div class="form-text">
Der Code wechselt alle 30 Sekunden.
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-check"></i>
Zwei-Faktor-Authentifizierung aktivieren
</button>
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-outline-secondary">
<i class="fas fa-times"></i>
Abbrechen
</a>
</div>
</form>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="alert alert-info">
<h6><i class="fas fa-info-circle"></i> Wichtige Hinweise:</h6>
<ul class="mb-0 small">
<li>Nach der Aktivierung erhalten Sie Backup-Codes für den Notfall</li>
<li>Bewahren Sie diese Backup-Codes sicher auf</li>
<li>Sie benötigen bei jeder Anmeldung einen Code aus der App</li>
<li>Die Zwei-Faktor-Authentifizierung erhöht die Sicherheit Ihres Kontos erheblich</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-focus on token input
const tokenInput = document.getElementById('token');
if (tokenInput) {
tokenInput.focus();
// Auto-submit when 6 digits entered
tokenInput.addEventListener('input', function() {
if (this.value.length === 6 && /^\d{6}$/.test(this.value)) {
// Small delay to allow user to see complete input
setTimeout(() => {
this.closest('form').submit();
}, 300);
}
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,89 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card shadow">
<div class="card-header text-center">
<h4 class="mb-0">
<i class="fas fa-shield-alt text-primary"></i>
Zwei-Faktor-Authentifizierung
</h4>
</div>
<div class="card-body">
<p class="text-center text-muted mb-4">
Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein
oder verwenden Sie einen Backup-Code.
</p>
<form method="post">
{% csrf_token %}
{% if next %}
<input type="hidden" name="next" value="{{ next }}">
{% endif %}
<div class="mb-4">
<label for="otp_token" class="form-label">Authentifizierungscode</label>
<input type="text"
class="form-control form-control-lg text-center"
id="otp_token"
name="otp_token"
placeholder="000000"
maxlength="8"
required
autocomplete="off"
autofocus>
<div class="form-text text-center">
6-stelliger Code aus der App oder 8-stelliger Backup-Code
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fas fa-sign-in-alt"></i>
Bestätigen
</button>
</div>
</form>
<div class="text-center mt-4">
<details>
<summary class="text-muted small">Probleme beim Anmelden?</summary>
<div class="mt-2 small text-muted">
<p>Falls Sie keinen Zugriff auf Ihre Authenticator-App haben:</p>
<ul class="text-start">
<li>Verwenden Sie einen der 8-stelligen Backup-Codes</li>
<li>Kontaktieren Sie den Administrator</li>
</ul>
</div>
</details>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tokenInput = document.getElementById('otp_token');
if (tokenInput) {
// Auto-submit when 6 digits entered (TOTP) or 8 characters (backup code)
tokenInput.addEventListener('input', function() {
const value = this.value.trim();
if ((value.length === 6 && /^\d{6}$/.test(value)) ||
(value.length === 8 && /^[a-f0-9]{8}$/i.test(value))) {
// Small delay to allow user to see complete input
setTimeout(() => {
this.closest('form').submit();
}, 300);
}
});
}
});
</script>
{% endblock %}