Add CORS testing documentation and HTML test page for Passkey Cross-Device Authentication
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 55s
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 55s
Introduce a comprehensive CORS testing guide in CORS_TEST_ANLEITUNG.md, detailing steps for testing OPTIONS and POST requests, along with expected responses. Additionally, add a new HTML test page (test-cors.html) to facilitate interactive testing of CORS headers and responses for the Passkey registration API. Update the server API to ensure proper CORS headers are set for Cross-Device Authentication, enhancing the overall testing and debugging process.
This commit is contained in:
124
CORS_TEST_ANLEITUNG.md
Normal file
124
CORS_TEST_ANLEITUNG.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# CORS-Test für Passkey Cross-Device Authentication
|
||||||
|
|
||||||
|
## Test-Datei verwenden
|
||||||
|
|
||||||
|
1. **Öffne die Test-Seite:**
|
||||||
|
```
|
||||||
|
https://harheimertc.tsschulz.de/test-cors.html
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Führe die Tests aus:**
|
||||||
|
- Klicke auf "Test OPTIONS Request" - sollte Status 204 zurückgeben
|
||||||
|
- Klicke auf "Test POST Request" - sollte Status 200 zurückgeben
|
||||||
|
- Klicke auf "Test /api/auth/register-passkey-options" - sollte Options zurückgeben
|
||||||
|
|
||||||
|
3. **Prüfe die Browser-Entwicklertools:**
|
||||||
|
- Öffne F12 → Network Tab
|
||||||
|
- Führe die Tests aus
|
||||||
|
- Klicke auf jeden Request und prüfe die **Response Headers**:
|
||||||
|
- `Access-Control-Allow-Origin` sollte `https://harheimertc.tsschulz.de` sein
|
||||||
|
- `Access-Control-Allow-Credentials` sollte `true` sein
|
||||||
|
- `Access-Control-Allow-Methods` sollte `GET, POST, OPTIONS` enthalten
|
||||||
|
|
||||||
|
## Manueller Test mit curl
|
||||||
|
|
||||||
|
### OPTIONS Preflight Test:
|
||||||
|
```bash
|
||||||
|
curl -X OPTIONS \
|
||||||
|
-H "Origin: https://harheimertc.tsschulz.de" \
|
||||||
|
-H "Access-Control-Request-Method: POST" \
|
||||||
|
-H "Access-Control-Request-Headers: Content-Type" \
|
||||||
|
-v \
|
||||||
|
https://harheimertc.tsschulz.de/api/auth/register-passkey-options
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erwartete Response:**
|
||||||
|
- Status: 204 No Content
|
||||||
|
- Header: `Access-Control-Allow-Origin: https://harheimertc.tsschulz.de`
|
||||||
|
- Header: `Access-Control-Allow-Credentials: true`
|
||||||
|
|
||||||
|
### POST Request Test:
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
-H "Origin: https://harheimertc.tsschulz.de" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Test","email":"test@example.com"}' \
|
||||||
|
-v \
|
||||||
|
https://harheimertc.tsschulz.de/api/auth/register-passkey-options
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erwartete Response:**
|
||||||
|
- Status: 200 OK
|
||||||
|
- Header: `Access-Control-Allow-Origin: https://harheimertc.tsschulz.de`
|
||||||
|
- Body: JSON mit `success: true` und `options` Objekt
|
||||||
|
|
||||||
|
## Browser Console Test
|
||||||
|
|
||||||
|
Öffne die Browser-Konsole (F12) und führe aus:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Test 1: OPTIONS Preflight
|
||||||
|
fetch('https://harheimertc.tsschulz.de/api/auth/register-passkey-options', {
|
||||||
|
method: 'OPTIONS',
|
||||||
|
headers: {
|
||||||
|
'Origin': window.location.origin,
|
||||||
|
'Access-Control-Request-Method': 'POST',
|
||||||
|
'Access-Control-Request-Headers': 'Content-Type'
|
||||||
|
}
|
||||||
|
}).then(r => {
|
||||||
|
console.log('OPTIONS Status:', r.status);
|
||||||
|
console.log('CORS Headers:', {
|
||||||
|
origin: r.headers.get('Access-Control-Allow-Origin'),
|
||||||
|
credentials: r.headers.get('Access-Control-Allow-Credentials'),
|
||||||
|
methods: r.headers.get('Access-Control-Allow-Methods')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: POST Request
|
||||||
|
fetch('https://harheimertc.tsschulz.de/api/auth/register-passkey-options', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Origin': window.location.origin
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com'
|
||||||
|
})
|
||||||
|
}).then(r => r.json()).then(data => {
|
||||||
|
console.log('POST Response:', data);
|
||||||
|
console.log('Has Options:', !!data.options);
|
||||||
|
console.log('Has Challenge:', !!data.options?.challenge);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Was zu prüfen ist:
|
||||||
|
|
||||||
|
1. **OPTIONS Request:**
|
||||||
|
- Status sollte 204 sein (nicht 200 oder 404)
|
||||||
|
- CORS-Header müssen vorhanden sein
|
||||||
|
|
||||||
|
2. **POST Request:**
|
||||||
|
- Status sollte 200 sein
|
||||||
|
- CORS-Header müssen vorhanden sein
|
||||||
|
- Response sollte `success: true` und `options` enthalten
|
||||||
|
|
||||||
|
3. **Cross-Device Problem:**
|
||||||
|
- Wenn CORS korrekt ist, aber Cross-Device trotzdem nicht funktioniert:
|
||||||
|
- Prüfe, ob der QR-Code die richtige Origin enthält
|
||||||
|
- Prüfe, ob das Smartphone die gleiche Origin erreichen kann
|
||||||
|
- Prüfe die Browser-Logs auf dem Smartphone (falls möglich)
|
||||||
|
|
||||||
|
## Häufige Probleme:
|
||||||
|
|
||||||
|
1. **Apache überschreibt CORS-Header:**
|
||||||
|
- Lösung: In Apache-Config sicherstellen, dass CORS-Header nicht überschrieben werden
|
||||||
|
|
||||||
|
2. **OPTIONS gibt 404 zurück:**
|
||||||
|
- Lösung: Endpoint muss OPTIONS-Requests explizit behandeln
|
||||||
|
|
||||||
|
3. **CORS-Header fehlen:**
|
||||||
|
- Lösung: Server-Endpoint muss CORS-Header setzen
|
||||||
|
|
||||||
|
4. **Origin-Mismatch:**
|
||||||
|
- Lösung: `WEBAUTHN_ORIGIN` muss exakt mit der Browser-Origin übereinstimmen
|
||||||
@@ -505,8 +505,7 @@ const handleRegisterWithPasskey = async () => {
|
|||||||
const webauthnStart = Date.now()
|
const webauthnStart = Date.now()
|
||||||
|
|
||||||
const mod = await import('@simplewebauthn/browser')
|
const mod = await import('@simplewebauthn/browser')
|
||||||
// startRegistration erwartet die Options direkt
|
// startRegistration erwartet die Options direkt (wie in anderen Dateien auch)
|
||||||
// @simplewebauthn/browser v13+ erwartet die Options direkt
|
|
||||||
let credential
|
let credential
|
||||||
try {
|
try {
|
||||||
// Timeout-Warnung nach 2 Minuten
|
// Timeout-Warnung nach 2 Minuten
|
||||||
@@ -517,29 +516,11 @@ const handleRegisterWithPasskey = async () => {
|
|||||||
console.warn('[DEBUG] Challenge:', pre.options?.challenge)
|
console.warn('[DEBUG] Challenge:', pre.options?.challenge)
|
||||||
}, 120000)
|
}, 120000)
|
||||||
|
|
||||||
// Stelle sicher, dass die Options korrekt formatiert sind
|
|
||||||
// @simplewebauthn/browser v13+ erwartet die Options direkt als Objekt
|
|
||||||
const registrationOptions = {
|
|
||||||
challenge: pre.options.challenge,
|
|
||||||
rp: pre.options.rp,
|
|
||||||
user: pre.options.user,
|
|
||||||
pubKeyCredParams: pre.options.pubKeyCredParams,
|
|
||||||
timeout: pre.options.timeout,
|
|
||||||
attestation: pre.options.attestation || 'none',
|
|
||||||
excludeCredentials: pre.options.excludeCredentials || [],
|
|
||||||
authenticatorSelection: pre.options.authenticatorSelection,
|
|
||||||
extensions: pre.options.extensions || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[DEBUG] startRegistration called - QR-Code should appear now (if Cross-Device)')
|
console.log('[DEBUG] startRegistration called - QR-Code should appear now (if Cross-Device)')
|
||||||
console.log('[DEBUG] Registration options structure:', {
|
console.log('[DEBUG] Passing options directly to startRegistration (same as in profil.vue)')
|
||||||
hasChallenge: !!registrationOptions.challenge,
|
|
||||||
hasRp: !!registrationOptions.rp,
|
|
||||||
hasUser: !!registrationOptions.user,
|
|
||||||
timeout: registrationOptions.timeout
|
|
||||||
})
|
|
||||||
|
|
||||||
credential = await mod.startRegistration(registrationOptions)
|
// Direkt die Options übergeben (wie in profil.vue und passkey-wiederherstellen.vue)
|
||||||
|
credential = await mod.startRegistration(pre.options)
|
||||||
|
|
||||||
clearTimeout(timeoutWarning)
|
clearTimeout(timeoutWarning)
|
||||||
|
|
||||||
|
|||||||
214
public/test-cors.html
Normal file
214
public/test-cors.html
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>CORS Test für Passkey Cross-Device</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: monospace; padding: 20px; }
|
||||||
|
.test { margin: 20px 0; padding: 10px; border: 1px solid #ccc; }
|
||||||
|
.success { background: #d4edda; }
|
||||||
|
.error { background: #f8d7da; }
|
||||||
|
.info { background: #d1ecf1; }
|
||||||
|
button { padding: 10px 20px; margin: 5px; }
|
||||||
|
pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>CORS Test für Passkey Cross-Device Authentication</h1>
|
||||||
|
|
||||||
|
<div class="test info">
|
||||||
|
<h3>1. Origin-Info</h3>
|
||||||
|
<p><strong>Current Origin:</strong> <span id="currentOrigin"></span></p>
|
||||||
|
<p><strong>Is Secure Context:</strong> <span id="isSecure"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>2. OPTIONS Preflight Test</h3>
|
||||||
|
<button onclick="testOptions()">Test OPTIONS Request</button>
|
||||||
|
<pre id="optionsResult"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>3. POST Request Test (mit Origin-Header)</h3>
|
||||||
|
<button onclick="testPost()">Test POST Request</button>
|
||||||
|
<pre id="postResult"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>4. Registration Options Test</h3>
|
||||||
|
<button onclick="testRegistrationOptions()">Test /api/auth/register-passkey-options</button>
|
||||||
|
<pre id="registrationResult"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>5. CORS Headers Check (Network Tab)</h3>
|
||||||
|
<p>Öffne die Browser-Entwicklertools (F12) → Network Tab</p>
|
||||||
|
<p>Führe die Tests oben aus und prüfe:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>OPTIONS Request:</strong> Sollte Status 204 haben</li>
|
||||||
|
<li><strong>Response Headers:</strong> Sollten enthalten:
|
||||||
|
<ul>
|
||||||
|
<li>Access-Control-Allow-Origin: <span id="currentOrigin2"></span></li>
|
||||||
|
<li>Access-Control-Allow-Credentials: true</li>
|
||||||
|
<li>Access-Control-Allow-Methods: GET, POST, OPTIONS</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const origin = window.location.origin;
|
||||||
|
document.getElementById('currentOrigin').textContent = origin;
|
||||||
|
document.getElementById('currentOrigin2').textContent = origin;
|
||||||
|
document.getElementById('isSecure').textContent = window.isSecureContext ? 'JA ✓' : 'NEIN ✗';
|
||||||
|
|
||||||
|
async function testOptions() {
|
||||||
|
const resultEl = document.getElementById('optionsResult');
|
||||||
|
resultEl.textContent = 'Testing OPTIONS request...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/register-passkey-options', {
|
||||||
|
method: 'OPTIONS',
|
||||||
|
headers: {
|
||||||
|
'Origin': origin,
|
||||||
|
'Access-Control-Request-Method': 'POST',
|
||||||
|
'Access-Control-Request-Headers': 'Content-Type'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
resultEl.innerHTML = `
|
||||||
|
Status: ${response.status} ${response.status === 204 ? '✓' : '✗'}
|
||||||
|
Status Text: ${response.statusText}
|
||||||
|
|
||||||
|
Response Headers:
|
||||||
|
${JSON.stringify(headers, null, 2)}
|
||||||
|
|
||||||
|
CORS Headers Check:
|
||||||
|
- 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 ✗'}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (response.status === 204 && headers['access-control-allow-origin']) {
|
||||||
|
resultEl.parentElement.className = 'test success';
|
||||||
|
} else {
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultEl.textContent = `ERROR: ${error.message}\n${error.stack}`;
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPost() {
|
||||||
|
const resultEl = document.getElementById('postResult');
|
||||||
|
resultEl.textContent = 'Testing POST request...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/register-passkey-options', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Origin': origin
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({ error: 'Could not parse JSON' }));
|
||||||
|
|
||||||
|
resultEl.innerHTML = `
|
||||||
|
Status: ${response.status}
|
||||||
|
Status Text: ${response.statusText}
|
||||||
|
|
||||||
|
Response Headers:
|
||||||
|
${JSON.stringify(headers, null, 2)}
|
||||||
|
|
||||||
|
Response Body:
|
||||||
|
${JSON.stringify(data, null, 2)}
|
||||||
|
|
||||||
|
CORS Headers Check:
|
||||||
|
- Access-Control-Allow-Origin: ${headers['access-control-allow-origin'] || 'FEHLT ✗'}
|
||||||
|
- Access-Control-Allow-Credentials: ${headers['access-control-allow-credentials'] || 'FEHLT ✗'}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (headers['access-control-allow-origin']) {
|
||||||
|
resultEl.parentElement.className = 'test success';
|
||||||
|
} else {
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultEl.textContent = `ERROR: ${error.message}\n${error.stack}`;
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testRegistrationOptions() {
|
||||||
|
const resultEl = document.getElementById('registrationResult');
|
||||||
|
resultEl.textContent = 'Testing registration options endpoint...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/register-passkey-options', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
phone: ''
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
resultEl.innerHTML = `
|
||||||
|
Status: ${response.status}
|
||||||
|
Status Text: ${response.statusText}
|
||||||
|
|
||||||
|
Response Headers (CORS):
|
||||||
|
- 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 ✗'}
|
||||||
|
|
||||||
|
Response Body:
|
||||||
|
${JSON.stringify(data, null, 2)}
|
||||||
|
|
||||||
|
Options Structure:
|
||||||
|
- hasChallenge: ${data.options?.challenge ? 'JA ✓' : 'NEIN ✗'}
|
||||||
|
- hasRp: ${data.options?.rp ? 'JA ✓' : 'NEIN ✗'}
|
||||||
|
- hasUser: ${data.options?.user ? 'JA ✓' : 'NEIN ✗'}
|
||||||
|
- rpId: ${data.options?.rp?.id || 'FEHLT'}
|
||||||
|
- timeout: ${data.options?.timeout || 'FEHLT'}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (data.success && data.options && headers['access-control-allow-origin']) {
|
||||||
|
resultEl.parentElement.className = 'test success';
|
||||||
|
} else {
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultEl.textContent = `ERROR: ${error.message}\n${error.stack}`;
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -125,17 +125,28 @@ export default defineEventHandler(async (event) => {
|
|||||||
await writeAuditLog('auth.passkey.prereg.options', { email })
|
await writeAuditLog('auth.passkey.prereg.options', { email })
|
||||||
|
|
||||||
// CORS-Header für Cross-Device Authentication
|
// CORS-Header für Cross-Device Authentication
|
||||||
if (requestOrigin) {
|
// WICHTIG: Für Cross-Device muss CORS korrekt konfiguriert sein
|
||||||
setHeader(event, 'Access-Control-Allow-Origin', requestOrigin)
|
const allowedOrigin = requestOrigin || webauthnOrigin
|
||||||
|
|
||||||
|
if (allowedOrigin) {
|
||||||
|
setHeader(event, 'Access-Control-Allow-Origin', allowedOrigin)
|
||||||
setHeader(event, 'Access-Control-Allow-Credentials', 'true')
|
setHeader(event, 'Access-Control-Allow-Credentials', 'true')
|
||||||
setHeader(event, 'Access-Control-Allow-Methods', 'POST, OPTIONS')
|
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||||
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Origin, X-Requested-With')
|
||||||
console.log('[DEBUG] CORS headers set', { origin: requestOrigin })
|
setHeader(event, 'Access-Control-Max-Age', '86400') // 24 Stunden Cache für Preflight
|
||||||
|
console.log('[DEBUG] CORS headers set', {
|
||||||
|
origin: allowedOrigin,
|
||||||
|
requestOrigin,
|
||||||
|
webauthnOrigin,
|
||||||
|
method: getMethod(event)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OPTIONS Preflight-Request für Cross-Device
|
||||||
if (getMethod(event) === 'OPTIONS') {
|
if (getMethod(event) === 'OPTIONS') {
|
||||||
console.log('[DEBUG] OPTIONS request, returning early')
|
console.log('[DEBUG] OPTIONS preflight request, returning 204')
|
||||||
return { success: true }
|
setResponseStatus(event, 204)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stelle sicher, dass die Options korrekt serialisiert werden
|
// Stelle sicher, dass die Options korrekt serialisiert werden
|
||||||
|
|||||||
@@ -11,12 +11,30 @@ import { assertPasswordNotPwned } from '../../utils/hibp.js'
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const requestStart = Date.now()
|
const requestStart = Date.now()
|
||||||
const requestOrigin = getHeader(event, 'origin')
|
const requestOrigin = getHeader(event, 'origin')
|
||||||
|
const { origin: webauthnOrigin } = getWebAuthnConfig()
|
||||||
|
|
||||||
console.log('[DEBUG] register-passkey request received', {
|
console.log('[DEBUG] register-passkey request received', {
|
||||||
origin: requestOrigin,
|
origin: requestOrigin,
|
||||||
|
webauthnOrigin,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// CORS-Header für Cross-Device Authentication
|
||||||
|
const allowedOrigin = requestOrigin || webauthnOrigin
|
||||||
|
if (allowedOrigin) {
|
||||||
|
setHeader(event, 'Access-Control-Allow-Origin', allowedOrigin)
|
||||||
|
setHeader(event, 'Access-Control-Allow-Credentials', 'true')
|
||||||
|
setHeader(event, 'Access-Control-Allow-Methods', 'POST, OPTIONS')
|
||||||
|
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Origin, X-Requested-With')
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIONS Preflight-Request
|
||||||
|
if (getMethod(event) === 'OPTIONS') {
|
||||||
|
console.log('[DEBUG] OPTIONS preflight request, returning 204')
|
||||||
|
setResponseStatus(event, 204)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
const registrationId = String(body?.registrationId || '')
|
const registrationId = String(body?.registrationId || '')
|
||||||
const response = body?.credential
|
const response = body?.credential
|
||||||
|
|||||||
214
test-cors.html
Normal file
214
test-cors.html
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>CORS Test für Passkey Cross-Device</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: monospace; padding: 20px; }
|
||||||
|
.test { margin: 20px 0; padding: 10px; border: 1px solid #ccc; }
|
||||||
|
.success { background: #d4edda; }
|
||||||
|
.error { background: #f8d7da; }
|
||||||
|
.info { background: #d1ecf1; }
|
||||||
|
button { padding: 10px 20px; margin: 5px; }
|
||||||
|
pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>CORS Test für Passkey Cross-Device Authentication</h1>
|
||||||
|
|
||||||
|
<div class="test info">
|
||||||
|
<h3>1. Origin-Info</h3>
|
||||||
|
<p><strong>Current Origin:</strong> <span id="currentOrigin"></span></p>
|
||||||
|
<p><strong>Is Secure Context:</strong> <span id="isSecure"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>2. OPTIONS Preflight Test</h3>
|
||||||
|
<button onclick="testOptions()">Test OPTIONS Request</button>
|
||||||
|
<pre id="optionsResult"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>3. POST Request Test (mit Origin-Header)</h3>
|
||||||
|
<button onclick="testPost()">Test POST Request</button>
|
||||||
|
<pre id="postResult"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>4. Registration Options Test</h3>
|
||||||
|
<button onclick="testRegistrationOptions()">Test /api/auth/register-passkey-options</button>
|
||||||
|
<pre id="registrationResult"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>5. CORS Headers Check (Network Tab)</h3>
|
||||||
|
<p>Öffne die Browser-Entwicklertools (F12) → Network Tab</p>
|
||||||
|
<p>Führe die Tests oben aus und prüfe:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>OPTIONS Request:</strong> Sollte Status 204 haben</li>
|
||||||
|
<li><strong>Response Headers:</strong> Sollten enthalten:
|
||||||
|
<ul>
|
||||||
|
<li>Access-Control-Allow-Origin: <span id="currentOrigin2"></span></li>
|
||||||
|
<li>Access-Control-Allow-Credentials: true</li>
|
||||||
|
<li>Access-Control-Allow-Methods: GET, POST, OPTIONS</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const origin = window.location.origin;
|
||||||
|
document.getElementById('currentOrigin').textContent = origin;
|
||||||
|
document.getElementById('currentOrigin2').textContent = origin;
|
||||||
|
document.getElementById('isSecure').textContent = window.isSecureContext ? 'JA ✓' : 'NEIN ✗';
|
||||||
|
|
||||||
|
async function testOptions() {
|
||||||
|
const resultEl = document.getElementById('optionsResult');
|
||||||
|
resultEl.textContent = 'Testing OPTIONS request...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/register-passkey-options', {
|
||||||
|
method: 'OPTIONS',
|
||||||
|
headers: {
|
||||||
|
'Origin': origin,
|
||||||
|
'Access-Control-Request-Method': 'POST',
|
||||||
|
'Access-Control-Request-Headers': 'Content-Type'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
resultEl.innerHTML = `
|
||||||
|
Status: ${response.status} ${response.status === 204 ? '✓' : '✗'}
|
||||||
|
Status Text: ${response.statusText}
|
||||||
|
|
||||||
|
Response Headers:
|
||||||
|
${JSON.stringify(headers, null, 2)}
|
||||||
|
|
||||||
|
CORS Headers Check:
|
||||||
|
- 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 ✗'}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (response.status === 204 && headers['access-control-allow-origin']) {
|
||||||
|
resultEl.parentElement.className = 'test success';
|
||||||
|
} else {
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultEl.textContent = `ERROR: ${error.message}\n${error.stack}`;
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPost() {
|
||||||
|
const resultEl = document.getElementById('postResult');
|
||||||
|
resultEl.textContent = 'Testing POST request...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/register-passkey-options', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Origin': origin
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({ error: 'Could not parse JSON' }));
|
||||||
|
|
||||||
|
resultEl.innerHTML = `
|
||||||
|
Status: ${response.status}
|
||||||
|
Status Text: ${response.statusText}
|
||||||
|
|
||||||
|
Response Headers:
|
||||||
|
${JSON.stringify(headers, null, 2)}
|
||||||
|
|
||||||
|
Response Body:
|
||||||
|
${JSON.stringify(data, null, 2)}
|
||||||
|
|
||||||
|
CORS Headers Check:
|
||||||
|
- Access-Control-Allow-Origin: ${headers['access-control-allow-origin'] || 'FEHLT ✗'}
|
||||||
|
- Access-Control-Allow-Credentials: ${headers['access-control-allow-credentials'] || 'FEHLT ✗'}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (headers['access-control-allow-origin']) {
|
||||||
|
resultEl.parentElement.className = 'test success';
|
||||||
|
} else {
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultEl.textContent = `ERROR: ${error.message}\n${error.stack}`;
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testRegistrationOptions() {
|
||||||
|
const resultEl = document.getElementById('registrationResult');
|
||||||
|
resultEl.textContent = 'Testing registration options endpoint...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/register-passkey-options', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
phone: ''
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
resultEl.innerHTML = `
|
||||||
|
Status: ${response.status}
|
||||||
|
Status Text: ${response.statusText}
|
||||||
|
|
||||||
|
Response Headers (CORS):
|
||||||
|
- 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 ✗'}
|
||||||
|
|
||||||
|
Response Body:
|
||||||
|
${JSON.stringify(data, null, 2)}
|
||||||
|
|
||||||
|
Options Structure:
|
||||||
|
- hasChallenge: ${data.options?.challenge ? 'JA ✓' : 'NEIN ✗'}
|
||||||
|
- hasRp: ${data.options?.rp ? 'JA ✓' : 'NEIN ✗'}
|
||||||
|
- hasUser: ${data.options?.user ? 'JA ✓' : 'NEIN ✗'}
|
||||||
|
- rpId: ${data.options?.rp?.id || 'FEHLT'}
|
||||||
|
- timeout: ${data.options?.timeout || 'FEHLT'}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (data.success && data.options && headers['access-control-allow-origin']) {
|
||||||
|
resultEl.parentElement.className = 'test success';
|
||||||
|
} else {
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultEl.textContent = `ERROR: ${error.message}\n${error.stack}`;
|
||||||
|
resultEl.parentElement.className = 'test error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user