Remove deprecated Passkey-related documentation and test files
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 47s

This commit deletes several files related to Passkey functionality, including CORS_TEST_ANLEITUNG.md, CROSS_DEVICE_DEBUG.md, CROSS_DEVICE_PROBLEM_ZUSAMMENFASSUNG.md, SMARTPHONE_TEST_ANLEITUNG.md, test-cors.html, test-smartphone.html, and Vue components for Passkey registration and recovery. These removals are part of a broader effort to streamline the codebase and focus on core authentication methods while Passkey support is under review.
This commit is contained in:
Torsten Schulz (local)
2026-01-09 08:50:26 +01:00
parent 2800ac84fb
commit 333d5ad9bc
11 changed files with 6 additions and 1747 deletions

View File

@@ -1,124 +0,0 @@
# 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

View File

@@ -1,152 +0,0 @@
# Cross-Device Passkey Debugging
## Problem: Smartphone kann keine Verbindung aufbauen
CORS funktioniert jetzt korrekt (OPTIONS gibt 204 zurück), aber das Smartphone kann immer noch keine Verbindung aufbauen.
## Debugging-Schritte
### 1. Prüfe Server-Logs
Auf dem Server, während du versuchst, den QR-Code zu scannen:
```bash
pm2 logs harheimertc --lines 100 | grep -i "register-passkey\|OPTIONS\|CORS\|origin"
```
**Was zu prüfen ist:**
- Kommt ein Request vom Smartphone an? (Sollte `[DEBUG] register-passkey request received` zeigen)
- Welche Origin hat der Request? (Sollte `https://harheimertc.tsschulz.de` sein)
- Welcher User-Agent? (Sollte das Smartphone-Browser sein)
### 2. Prüfe Browser-Konsole (Desktop)
Öffne F12 → Console auf dem Desktop-Browser und prüfe:
- Wird der QR-Code angezeigt?
- Gibt es Fehlermeldungen?
- Was steht in den Debug-Logs?
### 3. Prüfe Smartphone-Browser
**Wichtig:** Wenn du den QR-Code scannst, öffnet das Smartphone die Website. Prüfe dort:
1. **Öffne die Website direkt auf dem Smartphone:**
```
https://harheimertc.tsschulz.de/registrieren
```
2. **Prüfe, ob die Website lädt** (sollte funktionieren, da du sagst, du kannst die Website vom Smartphone aus erreichen)
3. **Versuche, den QR-Code zu scannen** und prüfe:
- Öffnet sich die Website auf dem Smartphone?
- Gibt es Fehlermeldungen?
- Wird ein Request an den Server gesendet?
### 4. Netzwerk-Test
**Auf dem Smartphone:**
- Öffne die Website direkt: `https://harheimertc.tsschulz.de`
- Prüfe, ob sie lädt
- Prüfe, ob HTTPS korrekt funktioniert (keine Zertifikat-Fehler)
**Mit curl (vom Server aus):**
```bash
# Simuliere einen Request vom Smartphone
curl -X POST \
-H "Origin: https://harheimertc.tsschulz.de" \
-H "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)" \
-H "Content-Type: application/json" \
-d '{"registrationId":"test","credential":{}}' \
-v \
https://harheimertc.tsschulz.de/api/auth/register-passkey
```
### 5. Häufige Probleme
#### Problem 1: QR-Code wird nicht angezeigt
**Symptom:** Kein QR-Code erscheint im Browser
**Lösung:**
- Prüfe Browser-Konsole auf Fehler
- Prüfe, ob `startRegistration` aufgerufen wird
- Prüfe, ob die Options korrekt sind
#### Problem 2: QR-Code wird gescannt, aber Website öffnet sich nicht
**Symptom:** QR-Code wird gescannt, aber nichts passiert
**Lösung:**
- Prüfe, ob der QR-Code die richtige URL enthält
- Prüfe, ob das Smartphone die Website erreichen kann
- Prüfe, ob es Firewall-Blockierungen gibt
#### Problem 3: Website öffnet sich, aber Request kommt nicht an
**Symptom:** Website öffnet sich auf dem Smartphone, aber Server-Logs zeigen keinen Request
**Lösung:**
- Prüfe CORS-Header (sollte jetzt funktionieren)
- Prüfe, ob der Request wirklich gesendet wird (Browser-Entwicklertools auf dem Smartphone)
- Prüfe, ob Apache den Request weiterleitet
#### Problem 4: Request kommt an, aber Fehler
**Symptom:** Request kommt im Server-Log an, aber es gibt einen Fehler
**Lösung:**
- Prüfe die Fehlermeldung im Server-Log
- Prüfe, ob die Challenge korrekt ist
- Prüfe, ob die Origin übereinstimmt
### 6. Manueller Test
**Test 1: Direkte Website-Zugriff vom Smartphone**
```
https://harheimertc.tsschulz.de/registrieren
```
Sollte funktionieren.
**Test 2: API-Endpoint vom Smartphone**
Öffne auf dem Smartphone:
```
https://harheimertc.tsschulz.de/api/auth/register-passkey-options
```
Sollte einen JSON-Fehler zurückgeben (weil POST erwartet wird), aber kein 404.
**Test 3: OPTIONS-Request vom Smartphone**
Mit einem Tool wie "HTTP Request" auf dem Smartphone:
```
OPTIONS https://harheimertc.tsschulz.de/api/auth/register-passkey-options
Origin: https://harheimertc.tsschulz.de
```
Sollte 204 mit CORS-Headern zurückgeben.
### 7. Browser-spezifische Probleme
**Chrome/Edge:**
- Sollte Cross-Device unterstützen
- QR-Code sollte automatisch erscheinen
**Firefox:**
- Unterstützt Cross-Device möglicherweise nicht vollständig
- Prüfe Browser-Konsole auf Warnungen
**Safari (iOS):**
- Unterstützt Cross-Device
- QR-Code sollte automatisch erscheinen
### 8. Was die Logs zeigen sollten
**Wenn alles funktioniert:**
1. Desktop-Browser: `[DEBUG] startRegistration called - QR-Code should appear now`
2. Smartphone scannt QR-Code
3. Smartphone sendet Request: `[DEBUG] register-passkey request received` mit Smartphone User-Agent
4. Server verifiziert: `[DEBUG] Verification successful`
**Wenn es nicht funktioniert:**
- Kein Request vom Smartphone → Problem mit QR-Code oder Netzwerk
- Request kommt an, aber Fehler → Problem mit Challenge oder Origin
- Timeout → Problem mit Verbindung oder CORS
### 9. Nächste Schritte
1. **Prüfe Server-Logs** während des QR-Code-Scans
2. **Prüfe Browser-Konsole** auf dem Desktop
3. **Prüfe, ob der QR-Code angezeigt wird**
4. **Prüfe, ob das Smartphone die Website öffnet**
5. **Prüfe, ob ein Request ankommt**
Bitte teile die Ergebnisse dieser Tests, dann können wir das Problem weiter eingrenzen.

View File

@@ -1,171 +0,0 @@
# Cross-Device Passkey Problem - Zusammenfassung
## Aktueller Status
**Was funktioniert:**
- CORS ist korrekt konfiguriert (OPTIONS gibt 204 zurück)
- POST-Requests funktionieren (Status 200)
- Options werden korrekt generiert
- QR-Code wird angezeigt (Desktop-Browser)
- QR-Code enthält FIDO-URI (Format: `FIDO:/...`)
**Was nicht funktioniert:**
- Verbindung vom Smartphone zum Server wird nicht hergestellt
- "Verbindung mit einem anderen Gerät wird hergestellt" bleibt hängen
- Keine Requests vom Smartphone im Server-Log
- Smartphone öffnet keine Website nach QR-Code-Scan
## Wichtige Erkenntnis: Tunnel-Server
**FIDO Cross-Device verwendet Tunnel-Server:**
- `cable.ua5v.com` (Google)
- `cable.auth.com` (Apple)
**Wie funktioniert es:**
1. Desktop-Browser generiert QR-Code mit öffentlichem Schlüssel (FIDO-URI)
2. Smartphone scannt QR-Code
3. **Smartphone verbindet sich über Tunnel-Server mit Desktop-Browser** (nicht direkt mit dem Server!)
4. Desktop-Browser leitet Credential-Response an den Server weiter
**Voraussetzungen:**
- ✅ Beide Geräte müssen **Internetverbindung** haben
- ✅ Tunnel-Server müssen erreichbar sein (Firewall/Netzwerk: cable.ua5v.com, cable.auth.com)
- ⚠️ Bluetooth kann für physische Nähe-Bestätigung verwendet werden (abhängig vom Browser/Gerät)
- ⚠️ Einige Browser/Implementierungen erfordern Bluetooth für Cross-Device
## Mögliche Ursachen
### 1. WebAuthn API auf dem Smartphone
**Problem:** Die WebAuthn-API auf dem Smartphone kann die Verbindung nicht herstellen.
**Prüfung:**
- Öffne die Website direkt auf dem Smartphone: `https://harheimertc.tsschulz.de/registrieren`
- Versuche, einen Passkey zu registrieren (ohne QR-Code)
- Funktioniert das?
### 2. QR-Code-Inhalt
**Problem:** Der QR-Code enthält möglicherweise nicht die richtige URL oder Challenge.
**Prüfung:**
- Scanne den QR-Code mit einem QR-Code-Scanner
- Prüfe, welche URL/Informationen im QR-Code enthalten sind
- Sollte die Origin `https://harheimertc.tsschulz.de` enthalten
### 3. Browser-Kompatibilität
**Problem:** Nicht alle Browser unterstützen Cross-Device Passkeys gleich gut.
**Prüfung:**
- Welcher Browser wird auf dem Desktop verwendet? (Chrome, Firefox, Edge, Safari)
- Welcher Browser wird auf dem Smartphone verwendet?
- Chrome/Edge sollten am besten funktionieren
### 4. Tunnel-Server / Netzwerk/Firewall
**Problem:** Das Smartphone kann die Tunnel-Server nicht erreichen oder Bluetooth ist nicht aktiviert.
**Prüfung:**
- Ist Bluetooth auf beiden Geräten aktiviert? ⚠️ **WICHTIG für Cross-Device!**
- Können Tunnel-Server erreicht werden? (cable.ua5v.com, cable.auth.com)
- Sind die Geräte in physischer Nähe? (Bluetooth-Reichweite)
- Gibt es Firewall-Blockierungen für Tunnel-Server?
- Kann das Smartphone die Website direkt öffnen? ✅ (bereits bestätigt)
### 5. Options-Struktur
**Problem:** Die Warnung "startRegistration() was not called correctly" deutet auf ein Problem mit der Options-Struktur hin.
**Prüfung:**
- Prüfe Browser-Konsole (Desktop) auf die Debug-Logs
- Prüfe, ob die Options-Struktur korrekt ist
- Die Options sollten direkt von `@simplewebauthn/server` kommen (keine manuelle Serialisierung)
## Debugging-Schritte
### Schritt 1: Server-Logs prüfen
```bash
pm2 logs harheimertc --lines 200 | grep -i "register-passkey\|mobile\|smartphone\|user-agent"
```
**Was zu prüfen ist:**
- Kommt ein Request vom Smartphone an, wenn der QR-Code gescannt wird?
- Welcher User-Agent? (Sollte "Mobile", "iPhone", "Android" enthalten)
- Welche IP-Adresse? (Sollte die IP des Smartphones sein)
### Schritt 2: Browser-Konsole prüfen (Desktop)
**Öffne F12 → Console und prüfe:**
- Gibt es die Warnung "startRegistration() was not called correctly"?
- Was zeigen die Debug-Logs `[DEBUG] Full options structure check`?
- Wird der QR-Code angezeigt?
### Schritt 3: Smartphone direkt testen
**Öffne auf dem Smartphone:**
```
https://harheimertc.tsschulz.de/registrieren
```
**Versuche:**
- Registrierung mit Passkey (ohne QR-Code)
- Funktioniert das? (Sollte einen lokalen Authenticator verwenden)
### Schritt 4: QR-Code-Inhalt prüfen
**Scanne den QR-Code mit einem QR-Code-Scanner:**
- Welche URL/Informationen sind enthalten?
- Enthält er die richtige Origin?
- Enthält er die Challenge?
## Bekannte Probleme
### Problem 1: "startRegistration() was not called correctly"
**Ursache:** Die Options-Struktur ist nicht korrekt.
**Lösung:**
- Options werden jetzt direkt von `@simplewebauthn/server` zurückgegeben (keine manuelle Serialisierung)
- Prüfe Browser-Konsole auf die Debug-Logs
### Problem 2: Keine Requests vom Smartphone
**Ursache:** Cross-Device-Verbindung funktioniert nicht.
**Mögliche Lösungen:**
1. Prüfe, ob beide Geräte im gleichen Netzwerk sind
2. Prüfe, ob der Browser Cross-Device unterstützt (Chrome/Edge am besten)
3. Prüfe, ob es Firewall-Blockierungen gibt
4. Versuche einen anderen Browser
### Problem 3: Timeout
**Ursache:** Die Verbindung wird nicht innerhalb von 5 Minuten hergestellt.
**Lösung:**
- Prüfe, ob Requests ankommen
- Prüfe, ob CORS korrekt ist (✅ bereits bestätigt)
- Prüfe, ob die Origin übereinstimmt
## Nächste Schritte
1. **Prüfe Server-Logs** während des QR-Code-Scans:
- Kommt ein Request an?
- Welcher User-Agent?
- Welche Fehlermeldungen?
2. **Prüfe Browser-Konsole** (Desktop):
- Gibt es noch die Warnung?
- Was zeigen die Debug-Logs?
3. **Teste direkt auf dem Smartphone:**
- Öffne die Registrierungsseite
- Versuche, einen Passkey zu registrieren (ohne QR-Code)
- Funktioniert das?
4. **Teile die Ergebnisse:**
- Server-Logs
- Browser-Konsole (Desktop)
- Was passiert auf dem Smartphone?
## Wichtige Erkenntnisse
- ✅ CORS funktioniert (OPTIONS gibt 204 zurück)
- ✅ POST-Requests funktionieren (Status 200)
- ✅ Options werden korrekt generiert
- ✅ QR-Code wird angezeigt
- ✅ Website öffnet sich auf dem Smartphone
- ❌ Verbindung vom Smartphone zum Server wird nicht hergestellt
- ❌ Keine Requests vom Smartphone im Server-Log
**Das Problem liegt wahrscheinlich in der Cross-Device-Verbindung selbst, nicht in CORS oder der Server-Konfiguration.**

View File

@@ -1,98 +0,0 @@
# Smartphone Passkey Test - Anleitung
## Test-Seite öffnen
**Auf dem Smartphone öffnen:**
```
https://harheimertc.tsschulz.de/test-smartphone.html
```
## Was die Test-Seite zeigt
### 1. Basis-Informationen
- Aktuelle URL und Origin
- Ob HTTPS/Secure Context aktiv ist
- Ob WebAuthn unterstützt wird
- Browser und Plattform-Info
### 2. OPTIONS Preflight Test
- Testet, ob OPTIONS-Requests funktionieren
- Zeigt CORS-Header
- Sollte Status 204 zurückgeben
### 3. Registration Options Test
- Testet den `/api/auth/register-passkey-options` Endpoint
- Zeigt, ob Options korrekt zurückgegeben werden
- Zeigt CORS-Header
### 4. WebAuthn API Test
- Testet die komplette WebAuthn-Registrierung
- Zeigt, ob `startRegistration` funktioniert
- Zeigt, ob ein QR-Code erscheint (für Cross-Device)
### 5. Network Monitor
- Zeigt alle Requests, die gesendet werden
- Zeigt Status-Codes und URLs
## Was zu prüfen ist
### ✅ Erfolgreich, wenn:
- **Basis-Info:** "Is Secure Context: JA ✓" und "WebAuthn Support: JA ✓"
- **OPTIONS Test:** Status 204 und alle CORS-Header vorhanden
- **Registration Options:** Status 200, `success: true`, `hasChallenge: JA ✓`
- **WebAuthn Test:** Zeigt "SUCCESS ✓" oder QR-Code erscheint
### ❌ Probleme, wenn:
- **OPTIONS Test:** Status 404 oder CORS-Header fehlen
- **Registration Options:** Status 500 oder `success: false`
- **WebAuthn Test:** Fehler wie "NotSupportedError" oder Timeout
## Häufige Probleme
### Problem 1: "WebAuthn Support: NEIN ✗"
**Ursache:** Browser unterstützt WebAuthn nicht oder nicht in Secure Context
**Lösung:**
- Stelle sicher, dass HTTPS verwendet wird
- Prüfe, ob der Browser WebAuthn unterstützt (Chrome, Safari, Edge sollten funktionieren)
### Problem 2: OPTIONS gibt 404
**Ursache:** Endpoint behandelt OPTIONS nicht korrekt
**Lösung:** Sollte jetzt behoben sein mit `.options.js` Dateien
### Problem 3: CORS-Header fehlen
**Ursache:** Server setzt CORS-Header nicht
**Lösung:** Prüfe Server-Logs, ob CORS-Header gesetzt werden
### Problem 4: WebAuthn Test schlägt fehl
**Ursache:**
- Kein lokaler Authenticator verfügbar
- Cross-Device-Verbindung funktioniert nicht
- QR-Code wird nicht angezeigt
**Lösung:**
- Prüfe, ob ein QR-Code erscheint
- Prüfe, ob das Smartphone die Website erreichen kann
- Prüfe Server-Logs, ob Requests ankommen
## Nächste Schritte
1. **Öffne die Test-Seite auf dem Smartphone**
2. **Führe alle Tests aus**
3. **Screenshot oder kopiere die Ergebnisse**
4. **Prüfe Server-Logs während der Tests:**
```bash
pm2 logs harheimertc --lines 50
```
5. **Teile die Ergebnisse**, dann können wir das Problem weiter eingrenzen
## Zusätzliche Debug-Info
**Auf dem Smartphone:**
- Öffne die Browser-Entwicklertools (falls verfügbar)
- Prüfe den Network-Tab
- Prüfe die Console auf Fehler
**Auf dem Server:**
- Prüfe PM2-Logs während der Tests
- Prüfe Apache-Logs für Requests
- Prüfe, ob Requests ankommen

View File

@@ -1,142 +0,0 @@
<template>
<div class="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
<div class="max-w-md w-full bg-gray-800 rounded-lg p-6 space-y-4">
<h1 class="text-2xl font-bold text-center">Passkey-Registrierung</h1>
<div v-if="status === 'loading'" class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto mb-4"></div>
<p>Lade Registrierungsoptionen...</p>
</div>
<div v-else-if="status === 'waiting'" class="text-center">
<div class="animate-pulse text-4xl mb-4">🔐</div>
<p class="text-lg mb-2">Warte auf Passkey-Authentifizierung...</p>
<p class="text-sm text-gray-400">Bitte bestätigen Sie die Registrierung auf diesem Gerät.</p>
</div>
<div v-else-if="status === 'error'" class="text-center text-red-400">
<p class="text-lg font-semibold mb-2">Fehler</p>
<p>{{ errorMessage }}</p>
<button
@click="retry"
class="mt-4 px-4 py-2 bg-primary-600 hover:bg-primary-700 rounded"
>
Erneut versuchen
</button>
</div>
<div v-else-if="status === 'success'" class="text-center text-green-400">
<p class="text-lg font-semibold mb-2"> Erfolgreich!</p>
<p>Die Credential-Response wurde an den Desktop-Browser gesendet.</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const status = ref('loading')
const errorMessage = ref('')
const registrationId = ref('')
// Hole registrationId aus URL-Parameter
onMounted(async () => {
const route = useRoute()
registrationId.value = route.query.registrationId || ''
if (!registrationId.value) {
status.value = 'error'
errorMessage.value = 'Keine registrationId in der URL gefunden.'
return
}
await startRegistration()
})
async function startRegistration() {
try {
status.value = 'loading'
// Hole Options vom Server
console.log('[DEBUG] Fetching options for registrationId:', registrationId.value)
const optionsResponse = await fetch(`/api/auth/register-passkey-options/${registrationId.value}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
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')
}
console.log('[DEBUG] Options received:', {
hasChallenge: !!optionsData.options.challenge,
rpId: optionsData.options.rp?.id,
timeout: optionsData.options.timeout
})
// Importiere @simplewebauthn/browser
const mod = await import('@simplewebauthn/browser')
if (!mod.startRegistration) {
throw new Error('startRegistration ist nicht verfügbar')
}
status.value = 'waiting'
console.log('[DEBUG] Calling startRegistration on smartphone...')
// Rufe startRegistration auf
const credential = await mod.startRegistration({ optionsJSON: optionsData.options })
console.log('[DEBUG] Credential received:', {
id: credential.id,
type: credential.type
})
// Sende Credential-Response an den Server
const verifyResponse = await fetch('/api/auth/register-passkey', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
registrationId: registrationId.value,
credential
})
})
if (!verifyResponse.ok) {
const errorData = await verifyResponse.json().catch(() => ({}))
throw new Error(errorData.message || `Verification failed: ${verifyResponse.status}`)
}
const verifyData = await verifyResponse.json()
if (verifyData.success) {
status.value = 'success'
console.log('[DEBUG] Registration successful!')
} else {
throw new Error(verifyData.message || 'Registrierung fehlgeschlagen')
}
} catch (error) {
console.error('[DEBUG] Registration error:', error)
status.value = 'error'
errorMessage.value = error.message || 'Unbekannter Fehler'
}
}
function retry() {
status.value = 'loading'
startRegistration()
}
</script>

View File

@@ -1,191 +0,0 @@
<template>
<div class="min-h-full flex items-center justify-center py-16 px-4 sm:px-6 lg:px-8 bg-gray-50">
<div class="max-w-md w-full space-y-8">
<div class="text-center">
<h2 class="text-3xl font-display font-bold text-gray-900">
Passkey wiederherstellen
</h2>
<p class="mt-2 text-sm text-gray-600">
Fügen Sie einen neuen Passkey hinzu, wenn Sie Ihr Gerät gewechselt haben.
</p>
</div>
<div class="bg-white rounded-xl shadow-lg p-8">
<!-- Token Flow -->
<div v-if="token" class="space-y-4">
<div
v-if="errorMessage"
class="bg-red-50 border border-red-200 rounded-lg p-4"
>
<p class="text-sm text-red-800 flex items-center">
<AlertCircle :size="18" class="mr-2" />
{{ errorMessage }}
</p>
</div>
<div
v-if="successMessage"
class="bg-green-50 border border-green-200 rounded-lg p-4"
>
<p class="text-sm text-green-800 flex items-center">
<Check :size="18" class="mr-2" />
{{ successMessage }}
</p>
</div>
<button
type="button"
class="w-full px-6 py-3 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
:disabled="isLoading || !isPasskeySupported"
@click="addPasskeyViaToken"
>
<Loader2 v-if="isLoading" :size="20" class="mr-2 animate-spin" />
<span>
{{ isLoading ? 'Wird vorbereitet...' : (isPasskeySupported ? 'Neuen Passkey hinzufügen' : 'Passkeys nicht verfügbar') }}
</span>
</button>
<div class="text-center">
<NuxtLink to="/login" class="text-sm text-primary-600 hover:text-primary-700 font-medium">
Zurück zum Login
</NuxtLink>
</div>
</div>
<!-- Request Link Flow -->
<form v-else class="space-y-6" @submit.prevent="requestLink">
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
E-Mail-Adresse
</label>
<input
id="email"
v-model="email"
type="email"
required
autocomplete="email"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-600 focus:border-transparent transition-all"
placeholder="ihre-email@example.com"
>
</div>
<div
v-if="errorMessage"
class="bg-red-50 border border-red-200 rounded-lg p-4"
>
<p class="text-sm text-red-800 flex items-center">
<AlertCircle :size="18" class="mr-2" />
{{ errorMessage }}
</p>
</div>
<div
v-if="successMessage"
class="bg-green-50 border border-green-200 rounded-lg p-4"
>
<p class="text-sm text-green-800 flex items-center">
<Check :size="18" class="mr-2" />
{{ successMessage }}
</p>
</div>
<button
type="submit"
class="w-full px-6 py-3 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
:disabled="isLoading"
>
<Loader2 v-if="isLoading" :size="20" class="mr-2 animate-spin" />
<span>{{ isLoading ? 'Wird gesendet...' : 'Recovery-Link per E-Mail senden' }}</span>
</button>
<div class="text-center">
<NuxtLink to="/login" class="text-sm text-primary-600 hover:text-primary-700 font-medium">
Zurück zum Login
</NuxtLink>
</div>
</form>
</div>
<div class="bg-primary-50 border border-primary-100 rounded-lg p-4">
<p class="text-sm text-primary-800">
<Info :size="16" class="inline mr-1" />
Wir schicken immer die gleiche Rückmeldung, egal ob ein Konto existiert.
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { AlertCircle, Check, Loader2, Info } from 'lucide-vue-next'
const route = useRoute()
const token = ref(String(route.query.token || ''))
const email = ref('')
const isLoading = ref(false)
const errorMessage = ref('')
const successMessage = ref('')
const isPasskeySupported = ref(false)
if (process.client) {
isPasskeySupported.value = !!window.PublicKeyCredential
}
const requestLink = async () => {
errorMessage.value = ''
successMessage.value = ''
isLoading.value = true
try {
const res = await $fetch('/api/auth/passkeys/recovery/request', {
method: 'POST',
body: { email: email.value }
})
successMessage.value = res.message || 'Falls ein Konto existiert, wurde eine E-Mail gesendet.'
} catch (e) {
errorMessage.value = e?.data?.message || 'Fehler beim Senden der E-Mail.'
} finally {
isLoading.value = false
}
}
const addPasskeyViaToken = async () => {
errorMessage.value = ''
successMessage.value = ''
if (!isPasskeySupported.value) {
errorMessage.value = 'Passkeys sind in diesem Browser/unter dieser URL nicht verfügbar (HTTPS erforderlich).'
return
}
isLoading.value = true
try {
const opts = await $fetch('/api/auth/passkeys/recovery/options', {
method: 'GET',
query: { token: token.value }
})
const mod = await import('@simplewebauthn/browser')
// @simplewebauthn/browser v13+ erwartet { optionsJSON: options }
const credential = await mod.startRegistration({ optionsJSON: opts.options })
const res = await $fetch('/api/auth/passkeys/recovery/complete', {
method: 'POST',
body: {
recoveryId: opts.recoveryId,
credential
}
})
successMessage.value = res.message || 'Passkey hinzugefügt.'
} catch (e) {
errorMessage.value = e?.data?.message || e?.message || 'Passkey konnte nicht hinzugefügt werden.'
} finally {
isLoading.value = false
}
}
useHead({ title: 'Passkey wiederherstellen - Harheimer TC' })
</script>

View File

@@ -1,214 +0,0 @@
<!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>

View File

@@ -1,438 +0,0 @@
<!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>

View File

@@ -78,7 +78,9 @@ export default defineEventHandler(async (event) => {
})
const baseUrl = process.env.NUXT_PUBLIC_BASE_URL || 'http://localhost:3100'
const link = `${baseUrl}/passkey-wiederherstellen?token=${token}`
// Passkey-Wiederherstellungsseite vorläufig deaktiviert
// const link = `${baseUrl}/passkey-wiederherstellen?token=${token}`
const link = `${baseUrl}/login` // Fallback auf Login-Seite
await transporter.sendMail({
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',

View File

@@ -12,6 +12,7 @@ export default defineEventHandler((event) => {
const ip = getHeader(event, 'x-forwarded-for') || getHeader(event, 'x-real-ip') || 'unknown'
// Logge nur Passkey-relevante Endpoints (um Logs nicht zu überfluten)
// Passkey-Endpoints vorläufig deaktiviert
const passkeyEndpoints = [
'/api/auth/register-passkey',
'/api/auth/register-passkey-options',
@@ -19,8 +20,8 @@ export default defineEventHandler((event) => {
'/api/auth/passkeys/register',
'/api/auth/passkeys/authentication-options',
'/api/auth/passkeys/login',
'/api/auth/passkeys/recovery',
'/passkey-register-cross-device'
'/api/auth/passkeys/recovery'
// '/passkey-register-cross-device' - Seite gelöscht
]
// Logge auch alle Requests vom Smartphone (Mobile User-Agent)

View File

@@ -1,214 +0,0 @@
<!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>