Quelltext des aktuellen Standes
<?php $this->layout('layouts/auth', [
'page_title' => 'Zwei-Faktor-Authentifizierung',
// Mit zwei Varianten stehen zwei Umschalt-Knöpfe nebeneinander – dafür
// braucht die Karte mehr Breite als das Standard-Anmeldefenster.
'auth_wide' => !empty($has_totp ?? true) && !empty($has_webauthn ?? false),
]); ?>
<div class="auth-box-body">
<p class="auth-box-msg">
<?= $this->t('<strong>Zwei-Faktor-Authentifizierung</strong><br> <small style="color:var(--color-secondary);">Bestätige deine Identität mit einem zweiten Faktor.</small>') ?>
</p>
<?php if (!empty($error)): ?>
<div class="auth-alert auth-alert-error"><?= $this->e($error) ?></div>
<?php endif; ?>
<?php
$hasTotp = $has_totp ?? true;
$hasWebauthn = $has_webauthn ?? false;
// If both available, start with WebAuthn (more seamless UX)
$defaultTab = $hasWebauthn ? 'webauthn' : 'totp';
?>
<?php if ($hasTotp && $hasWebauthn): ?>
<!-- Tab switcher -->
<div style="display:flex;gap:.5rem;margin-bottom:1.25rem;">
<button id="tabBtnWebauthn" onclick="mfaTab('webauthn')"
class="btn btn-sm btn-block"
style="flex:1;border:1px solid var(--color-gray-300);border-radius:.25rem;padding:.4rem .5rem;font-size:.82rem;background:var(--color-primary);color:var(--color-on-accent);">
<?= $this->te('🔑 Sicherheitsschlüssel') ?>
</button>
<button id="tabBtnTotp" onclick="mfaTab('totp')"
class="btn btn-sm btn-block"
style="flex:1;border:1px solid var(--color-gray-300);border-radius:.25rem;padding:.4rem .5rem;font-size:.82rem;background:var(--color-bg);color:var(--color-gray-700);">
<?= $this->te('📱 Authenticator-App') ?>
</button>
</div>
<?php endif; ?>
<!-- ── WebAuthn panel ── -->
<?php if ($hasWebauthn): ?>
<div id="panelWebauthn" <?= $defaultTab !== 'webauthn' ? 'style="display:none;"' : '' ?>>
<div style="text-align:center;padding:.75rem 0 1.25rem;">
<div id="waIcon" style="font-size:2.5rem;margin-bottom:.5rem;line-height:1;">🔑</div>
<p id="waStatus" style="font-size:.875rem;color:var(--color-secondary);margin:0;">
<?= $this->te('Klicke auf den Button und berühre dann deinen Sicherheitsschlüssel.') ?>
</p>
<div id="waError" class="auth-alert auth-alert-error" style="display:none;margin-top:.75rem;text-align:left;"></div>
</div>
<button id="waBtn" type="button" class="btn btn-primary btn-block" onclick="mfaWebauthnStart()">
<?= $this->te('Sicherheitsschlüssel verwenden') ?>
</button>
</div>
<?php endif; ?>
<!-- ── TOTP panel ── -->
<?php if ($hasTotp): ?>
<div id="panelTotp" <?= $defaultTab !== 'totp' ? 'style="display:none;"' : '' ?>>
<form method="POST" action="/auth/mfa" id="mfaForm">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="auth-input-group" style="text-align:center;">
<input type="text" name="code" id="mfaCode"
class="form-control" inputmode="numeric" pattern="\d{6}"
maxlength="6" placeholder="000000" required
style="font-size:1.8rem;letter-spacing:.35em;text-align:center;font-weight:600;"
autocomplete="one-time-code" autofocus>
<small style="color:var(--color-secondary);font-size:.78rem;"><?= $this->te('6-stelliger Code aus deiner Authenticator-App') ?></small>
</div>
</form>
</div>
<?php endif; ?>
<!-- Backup-Code-Option -->
<div style="text-align:center;margin-top:1rem;border-top:1px solid var(--color-gray-300);padding-top:.75rem;">
<a href="#" id="showBackupCodeLink" style="font-size:.78rem;color:var(--color-secondary);"
onclick="document.getElementById('backupCodePanel').style.display='';this.style.display='none';return false;">
<?= $this->te('Backup-Code verwenden') ?>
</a>
<div id="backupCodePanel" style="display:none;margin-top:.75rem;">
<form method="POST" action="/auth/mfa/backup" id="backupForm">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="text" name="backup_code" class="form-control"
placeholder="XXXX-XXXX"
maxlength="9"
style="text-align:center;letter-spacing:.1em;font-weight:600;text-transform:uppercase;"
autocomplete="off">
<button type="submit" class="btn btn-outline-secondary btn-block" style="margin-top:.5rem;font-size:.85rem;">
<?= $this->te('Mit Backup-Code anmelden') ?>
</button>
<small style="color:var(--color-secondary);font-size:.75rem;"><?= $this->te('Jeder Backup-Code kann nur einmal verwendet werden.') ?></small>
</form>
</div>
</div>
<div style="text-align:center;margin-top:.75rem;">
<a href="/auth/login" style="font-size:.82rem;color:var(--color-secondary);">
<?= $this->te('← Zurück zur Anmeldung') ?>
</a>
</div>
</div>
<script>
<?php if ($hasTotp): ?>
// Auto-submit TOTP when 6 digits entered
var codeEl = document.getElementById('mfaCode');
if (codeEl) {
codeEl.addEventListener('input', function() {
var clean = this.value.replace(/\D/g,'');
this.value = clean;
if (clean.length === 6) document.getElementById('mfaForm').submit();
});
}
<?php endif; ?>
<?php if ($hasTotp && $has_webauthn): ?>
function mfaTab(tab) {
document.getElementById('panelWebauthn').style.display = tab === 'webauthn' ? '' : 'none';
document.getElementById('panelTotp').style.display = tab === 'totp' ? '' : 'none';
document.getElementById('tabBtnWebauthn').style.background = tab === 'webauthn' ? '#3c8dbc' : '#fff';
document.getElementById('tabBtnWebauthn').style.color = tab === 'webauthn' ? '#fff' : '#495057';
document.getElementById('tabBtnTotp').style.background = tab === 'totp' ? '#3c8dbc' : '#fff';
document.getElementById('tabBtnTotp').style.color = tab === 'totp' ? '#fff' : '#495057';
if (tab === 'totp' && document.getElementById('mfaCode')) {
document.getElementById('mfaCode').focus();
}
}
<?php endif; ?>
<?php if ($has_webauthn): ?>
function base64urlToBuffer(b64u) {
var b64 = b64u.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
var bin = atob(b64), buf = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
return buf.buffer;
}
function bufferToBase64url(buf) {
var bytes = new Uint8Array(buf);
var bin = '';
bytes.forEach(function(b) { bin += String.fromCharCode(b); });
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
async function mfaWebauthnStart() {
var btn = document.getElementById('waBtn');
var status = document.getElementById('waStatus');
var errEl = document.getElementById('waError');
var icon = document.getElementById('waIcon');
var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
btn.disabled = true;
btn.textContent = 'Warte auf Schlüssel…';
status.textContent = 'Berühre jetzt deinen Sicherheitsschlüssel.';
icon.textContent = '⏳';
errEl.style.display = 'none';
try {
// 1. Get challenge from server
var beginResp = await fetch('/auth/webauthn/begin', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': csrf},
body: '{}'
});
if (!beginResp.ok) {
var e = await beginResp.json();
throw new Error(e.error || 'Server-Fehler beim Starten der Authentifizierung.');
}
var getArgs = await beginResp.json();
// 2. Decode binary fields
getArgs.publicKey.challenge = base64urlToBuffer(getArgs.publicKey.challenge);
if (getArgs.publicKey.allowCredentials) {
getArgs.publicKey.allowCredentials = getArgs.publicKey.allowCredentials.map(function(c) {
return Object.assign({}, c, {id: base64urlToBuffer(c.id)});
});
}
// 3. Prompt user to touch key
var assertion = await navigator.credentials.get(getArgs);
// 4. Send assertion to server
var completeResp = await fetch('/auth/webauthn/complete', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': csrf},
body: JSON.stringify({
id: bufferToBase64url(assertion.rawId),
clientDataJSON: bufferToBase64url(assertion.response.clientDataJSON),
authenticatorData: bufferToBase64url(assertion.response.authenticatorData),
signature: bufferToBase64url(assertion.response.signature)
})
});
var result = await completeResp.json();
if (result.ok) {
icon.textContent = '✅';
status.textContent = 'Erfolgreich! Weiterleitung…';
window.location.href = result.redirect || '/dashboard';
} else {
throw new Error(result.error || 'Authentifizierung fehlgeschlagen.');
}
} catch (err) {
if (err.name === 'NotAllowedError') {
errEl.textContent = 'Abgebrochen oder Zeitüberschreitung. Bitte erneut versuchen.';
} else {
errEl.textContent = err.message || 'Ein Fehler ist aufgetreten.';
}
errEl.style.display = 'block';
icon.textContent = '🔑';
status.textContent = 'Klicke auf den Button und berühre dann deinen Sicherheitsschlüssel.';
btn.disabled = false;
btn.textContent = 'Sicherheitsschlüssel verwenden';
}
}
<?php endif; ?>
</script>