Update Passkey Registration to comply with @simplewebauthn/browser v13+ API
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s

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.
This commit is contained in:
Torsten Schulz (local)
2026-01-08 17:10:13 +01:00
parent 73ae8599c3
commit badf91afef
4 changed files with 63 additions and 11 deletions

View File

@@ -319,7 +319,8 @@ const addPasskey = async () => {
const name = window.prompt('Name für den Passkey (z.B. "iPhone", "Laptop"):', 'Passkey') || 'Passkey' const name = window.prompt('Name für den Passkey (z.B. "iPhone", "Laptop"):', 'Passkey') || 'Passkey'
const res = await $fetch('/api/auth/passkeys/registration-options', { method: 'POST' }) const res = await $fetch('/api/auth/passkeys/registration-options', { method: 'POST' })
const mod = await import('@simplewebauthn/browser') const mod = await import('@simplewebauthn/browser')
const credential = await mod.startRegistration(res.options) // @simplewebauthn/browser v13+ erwartet { optionsJSON: options }
const credential = await mod.startRegistration({ optionsJSON: res.options })
await $fetch('/api/auth/passkeys/register', { await $fetch('/api/auth/passkeys/register', {
method: 'POST', method: 'POST',
body: { credential, name } body: { credential, name }

View File

@@ -167,7 +167,8 @@ const addPasskeyViaToken = async () => {
}) })
const mod = await import('@simplewebauthn/browser') const mod = await import('@simplewebauthn/browser')
const credential = await mod.startRegistration(opts.options) // @simplewebauthn/browser v13+ erwartet { optionsJSON: options }
const credential = await mod.startRegistration({ optionsJSON: opts.options })
const res = await $fetch('/api/auth/passkeys/recovery/complete', { const res = await $fetch('/api/auth/passkeys/recovery/complete', {
method: 'POST', method: 'POST',

View File

@@ -445,27 +445,45 @@ const handleRegisterWithPasskey = async () => {
} }
// Debug: Prüfe die vollständige Options-Struktur // Debug: Prüfe die vollständige Options-Struktur
// WICHTIG: @simplewebauthn/browser erwartet:
// - challenge: Base64URL-String
// - user.id: Base64URL-String (wird automatisch zu Uint8Array konvertiert)
// - excludeCredentials[].id: Base64URL-String (wird automatisch zu Uint8Array konvertiert)
console.log('[DEBUG] Full options structure check:', { console.log('[DEBUG] Full options structure check:', {
hasChallenge: !!pre.options.challenge, hasChallenge: !!pre.options.challenge,
challengeValue: pre.options.challenge?.substring(0, 20) + '...', challengeValue: pre.options.challenge?.substring(0, 20) + '...',
challengeType: typeof pre.options.challenge, challengeType: typeof pre.options.challenge,
challengeLength: pre.options.challenge?.length,
hasRp: !!pre.options.rp, hasRp: !!pre.options.rp,
rpId: pre.options.rp?.id, rpId: pre.options.rp?.id,
rpName: pre.options.rp?.name, rpName: pre.options.rp?.name,
hasUser: !!pre.options.user, hasUser: !!pre.options.user,
userId: pre.options.user?.id ? 'present' : 'missing', userId: pre.options.user?.id ? 'present' : 'missing',
userIdType: typeof pre.options.user?.id, userIdType: typeof pre.options.user?.id,
userIdValue: typeof pre.options.user?.id === 'string' ? pre.options.user.id.substring(0, 20) + '...' : 'not a string',
userName: pre.options.user?.name, userName: pre.options.user?.name,
userDisplayName: pre.options.user?.displayName, userDisplayName: pre.options.user?.displayName,
hasPubKeyCredParams: !!pre.options.pubKeyCredParams, hasPubKeyCredParams: !!pre.options.pubKeyCredParams,
pubKeyCredParamsCount: pre.options.pubKeyCredParams?.length, pubKeyCredParamsCount: pre.options.pubKeyCredParams?.length,
hasAuthenticatorSelection: !!pre.options.authenticatorSelection, hasAuthenticatorSelection: !!pre.options.authenticatorSelection,
authenticatorSelection: pre.options.authenticatorSelection,
timeout: pre.options.timeout, timeout: pre.options.timeout,
timeoutType: typeof pre.options.timeout, timeoutType: typeof pre.options.timeout,
excludeCredentialsCount: pre.options.excludeCredentials?.length || 0,
hasExtensions: !!pre.options.extensions,
hasHints: !!pre.options.hints,
allKeys: Object.keys(pre.options), allKeys: Object.keys(pre.options),
optionsStringified: JSON.stringify(pre.options).substring(0, 200) + '...' userKeys: pre.options.user ? Object.keys(pre.options.user) : []
}) })
// Prüfe, ob user.id ein String ist (Base64URL)
// @simplewebauthn/browser erwartet user.id als Base64URL-String
if (pre.options.user?.id && typeof pre.options.user.id !== 'string') {
console.error('[DEBUG] ERROR: user.id is not a string!', typeof pre.options.user.id, pre.options.user.id)
console.error('[DEBUG] @simplewebauthn/browser erwartet user.id als Base64URL-String')
throw new Error('Invalid user.id format - must be Base64URL string')
}
console.log('[DEBUG] Calling startRegistration...') console.log('[DEBUG] Calling startRegistration...')
console.log('[DEBUG] Full options object (will be encoded in QR code for Cross-Device):', JSON.stringify(pre.options, null, 2)) console.log('[DEBUG] Full options object (will be encoded in QR code for Cross-Device):', JSON.stringify(pre.options, null, 2))
console.log('[DEBUG] Options summary for startRegistration:', { console.log('[DEBUG] Options summary for startRegistration:', {
@@ -526,8 +544,20 @@ const handleRegisterWithPasskey = async () => {
const webauthnStart = Date.now() const webauthnStart = Date.now()
// Importiere @simplewebauthn/browser
const mod = await import('@simplewebauthn/browser') const mod = await import('@simplewebauthn/browser')
// startRegistration erwartet die Options direkt (wie in anderen Dateien auch)
// Prüfe, ob startRegistration verfügbar ist
if (!mod.startRegistration) {
console.error('[DEBUG] ERROR: mod.startRegistration is not available!')
console.error('[DEBUG] Available exports:', Object.keys(mod))
throw new Error('startRegistration ist nicht verfügbar. Bitte prüfen Sie, ob @simplewebauthn/browser korrekt installiert ist.')
}
console.log('[DEBUG] @simplewebauthn/browser imported successfully')
console.log('[DEBUG] Available exports:', Object.keys(mod))
console.log('[DEBUG] startRegistration type:', typeof mod.startRegistration)
let credential let credential
try { try {
// Timeout-Warnung nach 2 Minuten // Timeout-Warnung nach 2 Minuten
@@ -563,24 +593,44 @@ const handleRegisterWithPasskey = async () => {
console.log('[DEBUG] Options validation before startRegistration:', { console.log('[DEBUG] Options validation before startRegistration:', {
hasChallenge: !!pre.options.challenge, hasChallenge: !!pre.options.challenge,
challengeType: typeof pre.options.challenge, challengeType: typeof pre.options.challenge,
challengeValue: pre.options.challenge?.substring(0, 20) + '...',
hasRp: !!pre.options.rp, hasRp: !!pre.options.rp,
hasRpId: !!pre.options.rp?.id, hasRpId: !!pre.options.rp?.id,
hasUser: !!pre.options.user, hasUser: !!pre.options.user,
hasUserID: !!pre.options.user?.id, hasUserID: !!pre.options.user?.id,
userIdType: typeof pre.options.user?.id,
userIdValue: typeof pre.options.user?.id === 'string' ? pre.options.user.id.substring(0, 20) + '...' : 'not a string',
timeout: pre.options.timeout, timeout: pre.options.timeout,
timeoutType: typeof pre.options.timeout, timeoutType: typeof pre.options.timeout,
allKeys: Object.keys(pre.options) allKeys: Object.keys(pre.options),
userKeys: pre.options.user ? Object.keys(pre.options.user) : []
}) })
// Stelle sicher, dass challenge ein String ist (nicht Base64URL-encoded) // Stelle sicher, dass challenge ein String ist (Base64URL)
if (typeof pre.options.challenge !== 'string') { if (typeof pre.options.challenge !== 'string') {
console.error('[DEBUG] ERROR: Challenge is not a string!', pre.options.challenge) console.error('[DEBUG] ERROR: Challenge is not a string!', typeof pre.options.challenge, pre.options.challenge)
throw new Error('Invalid challenge format') throw new Error('Invalid challenge format')
} }
// Direkt die Options übergeben (wie in profil.vue und passkey-wiederherstellen.vue) // Prüfe user.id - sollte ein String (Base64URL) sein
// @simplewebauthn/browser v13+ erwartet die Options direkt if (pre.options.user?.id && typeof pre.options.user.id !== 'string') {
credential = await mod.startRegistration(pre.options) console.warn('[DEBUG] WARNING: user.id is not a string!', typeof pre.options.user.id, pre.options.user.id)
console.warn('[DEBUG] @simplewebauthn/browser erwartet user.id als Base64URL-String')
}
// @simplewebauthn/browser v13+ erwartet { optionsJSON: options }
// Die Bibliothek unterstützt auch die alte API (direkt options), gibt aber eine Warnung
// Wir verwenden die neue API-Struktur, um die Warnung zu vermeiden
console.log('[DEBUG] Calling mod.startRegistration with new API structure...')
console.log('[DEBUG] Options structure:', {
challenge: pre.options.challenge?.substring(0, 20) + '...',
rpId: pre.options.rp?.id,
userId: typeof pre.options.user?.id === 'string' ? pre.options.user.id.substring(0, 20) + '...' : pre.options.user?.id,
timeout: pre.options.timeout
})
// Neue API-Struktur: { optionsJSON: options }
credential = await mod.startRegistration({ optionsJSON: pre.options })
clearTimeout(timeoutWarning) clearTimeout(timeoutWarning)

View File

@@ -384,7 +384,7 @@
const credential = await navigator.credentials.create({ const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions publicKey: publicKeyCredentialCreationOptions
}); });
dd
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
// Konvertiere Credential zu einem serialisierbaren Format // Konvertiere Credential zu einem serialisierbaren Format