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

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>