Enhance debug logging for Cross-Device Passkey Registration
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 50s
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:
394
public/test-smartphone.html
Normal file
394
public/test-smartphone.html
Normal 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>
|
||||
Reference in New Issue
Block a user