Files
harheimertc/public/test-smartphone.html
Torsten Schulz (local) badf91afef
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s
Update Passkey Registration to comply with @simplewebauthn/browser v13+ API
Refactor the Passkey registration logic in multiple components to utilize the new API structure requiring { optionsJSON: options }. Enhance debug logging to validate options, including checks for user ID format and challenge type. This update aims to improve compliance with the latest library requirements and provide better insights during the registration process.
2026-01-08 17:10:13 +01:00

439 lines
15 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 Registrierung...\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\n';
resultEl.textContent += 'HINWEIS: Diese Test-Seite verwendet die native WebAuthn-API.\n';
resultEl.textContent += 'Die eigentliche App verwendet @simplewebauthn/browser (ist im Build enthalten).\n\n';
// Verwende native WebAuthn-API (navigator.credentials.create)
// Dies ist die Basis-API, die @simplewebauthn/browser verwendet
const startTime = Date.now();
// Konvertiere user.id von Base64URL zu Uint8Array (falls nötig)
let userId;
if (typeof optionsData.options.user?.id === 'string') {
// Base64URL decode
const base64Url = optionsData.options.user.id;
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const binaryString = atob(base64);
userId = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
userId[i] = binaryString.charCodeAt(i);
}
} else if (optionsData.options.user?.id instanceof Uint8Array) {
userId = optionsData.options.user.id;
} else {
throw new Error('Invalid user.id format');
}
// Erstelle PublicKeyCredentialCreationOptions
const publicKeyCredentialCreationOptions = {
challenge: Uint8Array.from(atob(optionsData.options.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)),
rp: optionsData.options.rp,
user: {
id: userId,
name: optionsData.options.user.name,
displayName: optionsData.options.user.displayName
},
pubKeyCredParams: optionsData.options.pubKeyCredParams,
timeout: optionsData.options.timeout,
attestation: optionsData.options.attestation,
excludeCredentials: optionsData.options.excludeCredentials?.map(cred => ({
id: Uint8Array.from(atob(cred.id.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)),
type: cred.type,
transports: cred.transports
})) || [],
authenticatorSelection: optionsData.options.authenticatorSelection,
extensions: optionsData.options.extensions
};
console.log('[DEBUG] Calling navigator.credentials.create with options:', publicKeyCredentialCreationOptions);
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
});
dd
const duration = Date.now() - startTime;
// Konvertiere Credential zu einem serialisierbaren Format
const credentialData = {
id: credential.id,
rawId: Array.from(new Uint8Array(credential.rawId)),
type: credential.type,
response: {
clientDataJSON: Array.from(new Uint8Array(credential.response.clientDataJSON)),
attestationObject: Array.from(new Uint8Array(credential.response.attestationObject))
},
transports: credential.response.getTransports?.() || []
};
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': credentialData.transports || []
},
'Note': 'Dies ist die native WebAuthn-API. Die eigentliche App verwendet @simplewebauthn/browser.',
'Credential Data': credentialData
};
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>