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.
395 lines
13 KiB
HTML
395 lines
13 KiB
HTML
<!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>
|