Enhance debug logging for Cross-Device Passkey Registration
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 50s

Add detailed debug statements in the registrieren.vue component to provide insights into the QR-Code generation process and the Cross-Device authentication flow. Additionally, update the register-passkey API to log request details, including user agent and method, to improve troubleshooting capabilities and ensure clarity during the registration process.
This commit is contained in:
Torsten Schulz (local)
2026-01-08 11:56:57 +01:00
parent 0deddeca51
commit 750c05eac1
5 changed files with 667 additions and 1 deletions

152
CROSS_DEVICE_DEBUG.md Normal file
View File

@@ -0,0 +1,152 @@
# Cross-Device Passkey Debugging
## Problem: Smartphone kann keine Verbindung aufbauen
CORS funktioniert jetzt korrekt (OPTIONS gibt 204 zurück), aber das Smartphone kann immer noch keine Verbindung aufbauen.
## Debugging-Schritte
### 1. Prüfe Server-Logs
Auf dem Server, während du versuchst, den QR-Code zu scannen:
```bash
pm2 logs harheimertc --lines 100 | grep -i "register-passkey\|OPTIONS\|CORS\|origin"
```
**Was zu prüfen ist:**
- Kommt ein Request vom Smartphone an? (Sollte `[DEBUG] register-passkey request received` zeigen)
- Welche Origin hat der Request? (Sollte `https://harheimertc.tsschulz.de` sein)
- Welcher User-Agent? (Sollte das Smartphone-Browser sein)
### 2. Prüfe Browser-Konsole (Desktop)
Öffne F12 → Console auf dem Desktop-Browser und prüfe:
- Wird der QR-Code angezeigt?
- Gibt es Fehlermeldungen?
- Was steht in den Debug-Logs?
### 3. Prüfe Smartphone-Browser
**Wichtig:** Wenn du den QR-Code scannst, öffnet das Smartphone die Website. Prüfe dort:
1. **Öffne die Website direkt auf dem Smartphone:**
```
https://harheimertc.tsschulz.de/registrieren
```
2. **Prüfe, ob die Website lädt** (sollte funktionieren, da du sagst, du kannst die Website vom Smartphone aus erreichen)
3. **Versuche, den QR-Code zu scannen** und prüfe:
- Öffnet sich die Website auf dem Smartphone?
- Gibt es Fehlermeldungen?
- Wird ein Request an den Server gesendet?
### 4. Netzwerk-Test
**Auf dem Smartphone:**
- Öffne die Website direkt: `https://harheimertc.tsschulz.de`
- Prüfe, ob sie lädt
- Prüfe, ob HTTPS korrekt funktioniert (keine Zertifikat-Fehler)
**Mit curl (vom Server aus):**
```bash
# Simuliere einen Request vom Smartphone
curl -X POST \
-H "Origin: https://harheimertc.tsschulz.de" \
-H "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)" \
-H "Content-Type: application/json" \
-d '{"registrationId":"test","credential":{}}' \
-v \
https://harheimertc.tsschulz.de/api/auth/register-passkey
```
### 5. Häufige Probleme
#### Problem 1: QR-Code wird nicht angezeigt
**Symptom:** Kein QR-Code erscheint im Browser
**Lösung:**
- Prüfe Browser-Konsole auf Fehler
- Prüfe, ob `startRegistration` aufgerufen wird
- Prüfe, ob die Options korrekt sind
#### Problem 2: QR-Code wird gescannt, aber Website öffnet sich nicht
**Symptom:** QR-Code wird gescannt, aber nichts passiert
**Lösung:**
- Prüfe, ob der QR-Code die richtige URL enthält
- Prüfe, ob das Smartphone die Website erreichen kann
- Prüfe, ob es Firewall-Blockierungen gibt
#### Problem 3: Website öffnet sich, aber Request kommt nicht an
**Symptom:** Website öffnet sich auf dem Smartphone, aber Server-Logs zeigen keinen Request
**Lösung:**
- Prüfe CORS-Header (sollte jetzt funktionieren)
- Prüfe, ob der Request wirklich gesendet wird (Browser-Entwicklertools auf dem Smartphone)
- Prüfe, ob Apache den Request weiterleitet
#### Problem 4: Request kommt an, aber Fehler
**Symptom:** Request kommt im Server-Log an, aber es gibt einen Fehler
**Lösung:**
- Prüfe die Fehlermeldung im Server-Log
- Prüfe, ob die Challenge korrekt ist
- Prüfe, ob die Origin übereinstimmt
### 6. Manueller Test
**Test 1: Direkte Website-Zugriff vom Smartphone**
```
https://harheimertc.tsschulz.de/registrieren
```
Sollte funktionieren.
**Test 2: API-Endpoint vom Smartphone**
Öffne auf dem Smartphone:
```
https://harheimertc.tsschulz.de/api/auth/register-passkey-options
```
Sollte einen JSON-Fehler zurückgeben (weil POST erwartet wird), aber kein 404.
**Test 3: OPTIONS-Request vom Smartphone**
Mit einem Tool wie "HTTP Request" auf dem Smartphone:
```
OPTIONS https://harheimertc.tsschulz.de/api/auth/register-passkey-options
Origin: https://harheimertc.tsschulz.de
```
Sollte 204 mit CORS-Headern zurückgeben.
### 7. Browser-spezifische Probleme
**Chrome/Edge:**
- Sollte Cross-Device unterstützen
- QR-Code sollte automatisch erscheinen
**Firefox:**
- Unterstützt Cross-Device möglicherweise nicht vollständig
- Prüfe Browser-Konsole auf Warnungen
**Safari (iOS):**
- Unterstützt Cross-Device
- QR-Code sollte automatisch erscheinen
### 8. Was die Logs zeigen sollten
**Wenn alles funktioniert:**
1. Desktop-Browser: `[DEBUG] startRegistration called - QR-Code should appear now`
2. Smartphone scannt QR-Code
3. Smartphone sendet Request: `[DEBUG] register-passkey request received` mit Smartphone User-Agent
4. Server verifiziert: `[DEBUG] Verification successful`
**Wenn es nicht funktioniert:**
- Kein Request vom Smartphone → Problem mit QR-Code oder Netzwerk
- Request kommt an, aber Fehler → Problem mit Challenge oder Origin
- Timeout → Problem mit Verbindung oder CORS
### 9. Nächste Schritte
1. **Prüfe Server-Logs** während des QR-Code-Scans
2. **Prüfe Browser-Konsole** auf dem Desktop
3. **Prüfe, ob der QR-Code angezeigt wird**
4. **Prüfe, ob das Smartphone die Website öffnet**
5. **Prüfe, ob ein Request ankommt**
Bitte teile die Ergebnisse dieser Tests, dann können wir das Problem weiter eingrenzen.

View File

@@ -0,0 +1,98 @@
# Smartphone Passkey Test - Anleitung
## Test-Seite öffnen
**Auf dem Smartphone öffnen:**
```
https://harheimertc.tsschulz.de/test-smartphone.html
```
## Was die Test-Seite zeigt
### 1. Basis-Informationen
- Aktuelle URL und Origin
- Ob HTTPS/Secure Context aktiv ist
- Ob WebAuthn unterstützt wird
- Browser und Plattform-Info
### 2. OPTIONS Preflight Test
- Testet, ob OPTIONS-Requests funktionieren
- Zeigt CORS-Header
- Sollte Status 204 zurückgeben
### 3. Registration Options Test
- Testet den `/api/auth/register-passkey-options` Endpoint
- Zeigt, ob Options korrekt zurückgegeben werden
- Zeigt CORS-Header
### 4. WebAuthn API Test
- Testet die komplette WebAuthn-Registrierung
- Zeigt, ob `startRegistration` funktioniert
- Zeigt, ob ein QR-Code erscheint (für Cross-Device)
### 5. Network Monitor
- Zeigt alle Requests, die gesendet werden
- Zeigt Status-Codes und URLs
## Was zu prüfen ist
### ✅ Erfolgreich, wenn:
- **Basis-Info:** "Is Secure Context: JA ✓" und "WebAuthn Support: JA ✓"
- **OPTIONS Test:** Status 204 und alle CORS-Header vorhanden
- **Registration Options:** Status 200, `success: true`, `hasChallenge: JA ✓`
- **WebAuthn Test:** Zeigt "SUCCESS ✓" oder QR-Code erscheint
### ❌ Probleme, wenn:
- **OPTIONS Test:** Status 404 oder CORS-Header fehlen
- **Registration Options:** Status 500 oder `success: false`
- **WebAuthn Test:** Fehler wie "NotSupportedError" oder Timeout
## Häufige Probleme
### Problem 1: "WebAuthn Support: NEIN ✗"
**Ursache:** Browser unterstützt WebAuthn nicht oder nicht in Secure Context
**Lösung:**
- Stelle sicher, dass HTTPS verwendet wird
- Prüfe, ob der Browser WebAuthn unterstützt (Chrome, Safari, Edge sollten funktionieren)
### Problem 2: OPTIONS gibt 404
**Ursache:** Endpoint behandelt OPTIONS nicht korrekt
**Lösung:** Sollte jetzt behoben sein mit `.options.js` Dateien
### Problem 3: CORS-Header fehlen
**Ursache:** Server setzt CORS-Header nicht
**Lösung:** Prüfe Server-Logs, ob CORS-Header gesetzt werden
### Problem 4: WebAuthn Test schlägt fehl
**Ursache:**
- Kein lokaler Authenticator verfügbar
- Cross-Device-Verbindung funktioniert nicht
- QR-Code wird nicht angezeigt
**Lösung:**
- Prüfe, ob ein QR-Code erscheint
- Prüfe, ob das Smartphone die Website erreichen kann
- Prüfe Server-Logs, ob Requests ankommen
## Nächste Schritte
1. **Öffne die Test-Seite auf dem Smartphone**
2. **Führe alle Tests aus**
3. **Screenshot oder kopiere die Ergebnisse**
4. **Prüfe Server-Logs während der Tests:**
```bash
pm2 logs harheimertc --lines 50
```
5. **Teile die Ergebnisse**, dann können wir das Problem weiter eingrenzen
## Zusätzliche Debug-Info
**Auf dem Smartphone:**
- Öffne die Browser-Entwicklertools (falls verfügbar)
- Prüfe den Network-Tab
- Prüfe die Console auf Fehler
**Auf dem Server:**
- Prüfe PM2-Logs während der Tests
- Prüfe Apache-Logs für Requests
- Prüfe, ob Requests ankommen

View File

@@ -518,6 +518,24 @@ const handleRegisterWithPasskey = async () => {
console.log('[DEBUG] startRegistration called - QR-Code should appear now (if Cross-Device)')
console.log('[DEBUG] Passing options directly to startRegistration (same as in profil.vue)')
console.log('[DEBUG] Options for QR-Code:', {
rpId: pre.options.rp?.id,
rpName: pre.options.rp?.name,
challenge: pre.options.challenge?.substring(0, 20) + '...',
timeout: pre.options.timeout,
timeoutSeconds: Math.round(pre.options.timeout / 1000),
note: 'Der Browser generiert automatisch einen QR-Code. Das Smartphone muss diese Origin erreichen können: ' + window.location.origin
})
// WICHTIG: Für Cross-Device muss das Smartphone die gleiche Origin erreichen können
// Der QR-Code enthält die Challenge und die Server-Info
// Das Smartphone öffnet dann die Website und sendet die Credential-Response zurück
console.log('[DEBUG] Cross-Device Flow:')
console.log('[DEBUG] 1. Browser generiert QR-Code mit Challenge:', pre.options.challenge?.substring(0, 20) + '...')
console.log('[DEBUG] 2. Smartphone scannt QR-Code')
console.log('[DEBUG] 3. Smartphone muss diese URL erreichen können:', window.location.origin)
console.log('[DEBUG] 4. Smartphone sendet Credential-Response an:', window.location.origin + '/api/auth/register-passkey')
console.log('[DEBUG] 5. Server verifiziert die Response')
// Direkt die Options übergeben (wie in profil.vue und passkey-wiederherstellen.vue)
credential = await mod.startRegistration(pre.options)

394
public/test-smartphone.html Normal file
View File

@@ -0,0 +1,394 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Smartphone Passkey Test</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
padding: 20px;
background: #f5f5f5;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 20px;
font-size: 24px;
}
.test-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
background: #f9f9f9;
}
.test-section h2 {
font-size: 18px;
margin-bottom: 10px;
color: #555;
}
button {
background: #007bff;
color: white;
border: none;
padding: 12px 20px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
width: 100%;
margin: 10px 0;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.result {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
}
.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.loading {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.status {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
margin-left: 10px;
}
.status.ok { background: #28a745; color: white; }
.status.fail { background: #dc3545; color: white; }
.status.warn { background: #ffc107; color: black; }
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔍 Smartphone Passkey Test</h1>
<div class="test-section">
<h2>1. Basis-Informationen</h2>
<div id="basicInfo" class="result info"></div>
</div>
<div class="test-section">
<h2>2. OPTIONS Preflight Test</h2>
<button onclick="testOptions()">Test OPTIONS Request</button>
<div id="optionsResult" class="result"></div>
</div>
<div class="test-section">
<h2>3. Registration Options Test</h2>
<button onclick="testRegistrationOptions()">Test /api/auth/register-passkey-options</button>
<div id="registrationResult" class="result"></div>
</div>
<div class="test-section">
<h2>4. WebAuthn API Test</h2>
<button onclick="testWebAuthn()">Test WebAuthn startRegistration</button>
<div id="webauthnResult" class="result"></div>
</div>
<div class="test-section">
<h2>5. Network Monitor</h2>
<p style="font-size: 12px; color: #666;">
Öffne die Browser-Entwicklertools (falls verfügbar) oder prüfe die Netzwerk-Requests unten.
</p>
<div id="networkLog" class="result info" style="max-height: 200px;"></div>
</div>
</div>
<script>
const origin = window.location.origin;
const apiBase = origin;
// Basis-Informationen
function showBasicInfo() {
const info = {
'Current URL': window.location.href,
'Origin': origin,
'Protocol': window.location.protocol,
'Hostname': window.location.hostname,
'Port': window.location.port || 'default (443 for HTTPS)',
'Is Secure Context': window.isSecureContext ? 'JA ✓' : 'NEIN ✗',
'User Agent': navigator.userAgent,
'WebAuthn Support': window.PublicKeyCredential ? 'JA ✓' : 'NEIN ✗',
'Platform': navigator.platform,
'Screen': `${screen.width}x${screen.height}`
};
document.getElementById('basicInfo').textContent =
Object.entries(info).map(([key, value]) => `${key}: ${value}`).join('\n');
}
function logNetwork(url, method, status, headers) {
const log = document.getElementById('networkLog');
const time = new Date().toLocaleTimeString();
const entry = `[${time}] ${method} ${url}\n Status: ${status}\n Origin: ${origin}\n`;
log.textContent = entry + '\n' + log.textContent;
}
async function testOptions() {
const resultEl = document.getElementById('optionsResult');
resultEl.className = 'result loading';
resultEl.textContent = 'Testing OPTIONS request...';
const url = `${apiBase}/api/auth/register-passkey-options`;
try {
const startTime = Date.now();
const response = await fetch(url, {
method: 'OPTIONS',
headers: {
'Origin': origin,
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'Content-Type'
}
});
const duration = Date.now() - startTime;
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
logNetwork(url, 'OPTIONS', response.status, headers);
const result = {
'Status': `${response.status} ${response.status === 204 ? '✓' : '✗'}`,
'Status Text': response.statusText,
'Duration': `${duration}ms`,
'CORS Headers': {
'Access-Control-Allow-Origin': headers['access-control-allow-origin'] || 'FEHLT ✗',
'Access-Control-Allow-Credentials': headers['access-control-allow-credentials'] || 'FEHLT ✗',
'Access-Control-Allow-Methods': headers['access-control-allow-methods'] || 'FEHLT ✗',
'Access-Control-Allow-Headers': headers['access-control-allow-headers'] || 'FEHLT ✗'
},
'All Headers': headers
};
resultEl.textContent = JSON.stringify(result, null, 2);
if (response.status === 204 && headers['access-control-allow-origin']) {
resultEl.className = 'result success';
} else {
resultEl.className = 'result error';
}
} catch (error) {
logNetwork(url, 'OPTIONS', 'ERROR', {});
resultEl.className = 'result error';
resultEl.textContent = `ERROR: ${error.message}\n${error.stack}`;
}
}
async function testRegistrationOptions() {
const resultEl = document.getElementById('registrationResult');
resultEl.className = 'result loading';
resultEl.textContent = 'Testing registration options...';
const url = `${apiBase}/api/auth/register-passkey-options`;
try {
const startTime = Date.now();
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': origin
},
body: JSON.stringify({
name: 'Test User',
email: 'test@example.com',
phone: ''
})
});
const duration = Date.now() - startTime;
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
logNetwork(url, 'POST', response.status, headers);
const data = await response.json();
const result = {
'Status': `${response.status} ${response.status === 200 ? '✓' : '✗'}`,
'Duration': `${duration}ms`,
'CORS Headers': {
'Access-Control-Allow-Origin': headers['access-control-allow-origin'] || 'FEHLT ✗',
'Access-Control-Allow-Credentials': headers['access-control-allow-credentials'] || 'FEHLT ✗'
},
'Response': {
'Success': data.success ? 'JA ✓' : 'NEIN ✗',
'Has Options': !!data.options ? 'JA ✓' : 'NEIN ✗',
'Has Challenge': !!data.options?.challenge ? 'JA ✓' : 'NEIN ✗',
'RP ID': data.options?.rp?.id || 'FEHLT',
'Timeout': data.options?.timeout || 'FEHLT',
'Registration ID': data.registrationId || 'FEHLT'
},
'Full Response': data
};
resultEl.textContent = JSON.stringify(result, null, 2);
if (data.success && data.options && headers['access-control-allow-origin']) {
resultEl.className = 'result success';
} else {
resultEl.className = 'result error';
}
} catch (error) {
logNetwork(url, 'POST', 'ERROR', {});
resultEl.className = 'result error';
resultEl.textContent = `ERROR: ${error.message}\n${error.stack}`;
}
}
async function testWebAuthn() {
const resultEl = document.getElementById('webauthnResult');
resultEl.className = 'result loading';
resultEl.textContent = 'Testing WebAuthn API...';
if (!window.PublicKeyCredential) {
resultEl.className = 'result error';
resultEl.textContent = 'ERROR: WebAuthn wird nicht unterstützt in diesem Browser/Kontext.\n\nMögliche Gründe:\n- Nicht HTTPS\n- Nicht in Secure Context\n- Browser unterstützt WebAuthn nicht';
return;
}
try {
// Zuerst Options holen
const optionsResponse = await fetch(`${apiBase}/api/auth/register-passkey-options`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': origin
},
body: JSON.stringify({
name: 'Test User',
email: 'test@example.com',
phone: ''
})
});
if (!optionsResponse.ok) {
throw new Error(`Options request failed: ${optionsResponse.status}`);
}
const optionsData = await optionsResponse.json();
if (!optionsData.success || !optionsData.options) {
throw new Error('Invalid options response');
}
resultEl.textContent = 'Options erhalten. Starte WebAuthn startRegistration...\n\n';
resultEl.textContent += `Challenge: ${optionsData.options.challenge?.substring(0, 20)}...\n`;
resultEl.textContent += `RP ID: ${optionsData.options.rp?.id}\n`;
resultEl.textContent += `Timeout: ${optionsData.options.timeout}ms\n\n`;
resultEl.textContent += 'Warte auf Passkey-Authentifizierung...\n';
resultEl.textContent += '(Scanne QR-Code oder verwende lokalen Authenticator)\n';
// WebAuthn startRegistration
// Versuche, das Modul zu laden (kann fehlschlagen auf Smartphone)
let startRegistration;
try {
const mod = await import('https://unpkg.com/@simplewebauthn/browser@13.2.2/dist/bundle/index.umd.min.js');
startRegistration = mod.startRegistration;
} catch (importError) {
// Fallback: Versuche, es lokal zu laden (wenn auf der Seite verfügbar)
resultEl.textContent += '\n\nWARNUNG: Konnte @simplewebauthn/browser nicht laden.\n';
resultEl.textContent += 'Dies ist normal, wenn die Bibliothek nicht verfügbar ist.\n';
resultEl.textContent += 'Die Options wurden aber erfolgreich vom Server erhalten.\n\n';
resultEl.textContent += 'Options-Struktur:\n';
resultEl.textContent += JSON.stringify(optionsData.options, null, 2);
resultEl.className = 'result warn';
return;
}
const startTime = Date.now();
const credential = await startRegistration(optionsData.options);
const duration = Date.now() - startTime;
const result = {
'Status': 'SUCCESS ✓',
'Duration': `${duration}ms`,
'Credential': {
'ID': credential.id,
'Type': credential.type,
'Raw ID': credential.rawId ? 'present' : 'missing',
'Response': credential.response ? 'present' : 'missing',
'Transports': credential.transports || []
},
'Full Credential': credential
};
resultEl.className = 'result success';
resultEl.textContent = JSON.stringify(result, null, 2);
logNetwork(`${apiBase}/api/auth/register-passkey-options`, 'WebAuthn', 'SUCCESS', {});
} catch (error) {
resultEl.className = 'result error';
const errorInfo = {
'Error': error.name,
'Message': error.message,
'Stack': error.stack,
'Note': 'Dies könnte ein Cross-Device-Problem sein, wenn kein lokaler Authenticator verfügbar ist.'
};
resultEl.textContent = JSON.stringify(errorInfo, null, 2);
logNetwork(`${apiBase}/api/auth/register-passkey-options`, 'WebAuthn', 'ERROR', {});
}
}
// Beim Laden Basis-Info anzeigen
showBasicInfo();
</script>
</body>
</html>

View File

@@ -11,12 +11,16 @@ import { assertPasswordNotPwned } from '../../utils/hibp.js'
export default defineEventHandler(async (event) => {
const requestStart = Date.now()
const requestOrigin = getHeader(event, 'origin')
const userAgent = getHeader(event, 'user-agent')
const { origin: webauthnOrigin } = getWebAuthnConfig()
console.log('[DEBUG] register-passkey request received', {
origin: requestOrigin,
webauthnOrigin,
timestamp: new Date().toISOString()
userAgent: userAgent?.substring(0, 100),
timestamp: new Date().toISOString(),
method: getMethod(event),
note: 'Dieser Request sollte vom Smartphone kommen, wenn der QR-Code gescannt wurde'
})
// CORS-Header für Cross-Device Authentication