Skip to content

Fix DataCloneError when credentialRequestOptions is relayed by password-manager extensions - #1042

Open
Be-Mann wants to merge 1 commit into
nextcloud:mainfrom
Be-Mann:fix/webauthn-datacloneerror-995
Open

Fix DataCloneError when credentialRequestOptions is relayed by password-manager extensions#1042
Be-Mann wants to merge 1 commit into
nextcloud:mainfrom
Be-Mann:fix/webauthn-datacloneerror-995

Conversation

@Be-Mann

@Be-Mann Be-Mann commented Aug 21, 2026

Copy link
Copy Markdown

Fixes #995

credentialRequestOptions comes from the Pinia store, so nested values (e.g. allowCredentials[i].transports) are still Vue-reactive Proxy objects when passed to startAuthentication(). The native navigator.credentials.get() handles that fine via WebIDL conversion, but password-manager extensions that intercept the call and relay it via window.postMessage() (structured clone) choke on the Proxies with DataCloneError: ... could not be cloned. — confirmed with both Bitwarden and Enpass.

This unwraps the options into a plain, serializable object before handing them to startAuthentication(). JSON.parse(JSON.stringify(...)) is used instead of structuredClone() because structuredClone() fails on reactive Proxies for the same reason. The options are pure JSON (base64url strings), so the round-trip is lossless.

Root cause analysis and manual verification by @tschuegy and @Rumbelstilzchen in #995.

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

…rd-manager extensions

Fixes nextcloud#995

`credentialRequestOptions` comes from the Pinia store, so nested values (e.g. `allowCredentials[i].transports`) are still Vue-reactive `Proxy` objects when passed to `startAuthentication()`. The native `navigator.credentials.get()` handles that fine via WebIDL conversion, but password-manager extensions that intercept the call and relay it via `window.postMessage()` (structured clone) choke on the Proxies with `DataCloneError: ... could not be cloned.` — confirmed with both Bitwarden and Enpass.

This unwraps the options into a plain, serializable object before handing them to `startAuthentication()`. `JSON.parse(JSON.stringify(...))` is used instead of `structuredClone()` because `structuredClone()` fails on reactive Proxies for the same reason. The options are pure JSON (base64url strings), so the round-trip is lossless.

Root cause analysis and manual verification by @tschuegy and @Rumbelstilzchen in nextcloud#995.

Signed-off-by: Be-Mann <25839760+Be-Mann@users.noreply.github.com>
@kesselb kesselb added bug Something isn't working 2. developing Work in progress labels Aug 23, 2026
@kesselb

kesselb commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Thanks a lot, good finding 👍

I think it should also work, and if I would prefer that version, to flag the object as not reactive.

Could you please give the following patch a test?

Index: src/components/Challenge.vue
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/components/Challenge.vue b/src/components/Challenge.vue
--- a/src/components/Challenge.vue	(revision 1ef69d077f361d17126dec3c295d86107e1fff1f)
+++ b/src/components/Challenge.vue	(date 1787519209471)
@@ -94,7 +94,7 @@
 			let authResponse
 			try {
 				authResponse = await startAuthentication({
-					optionsJSON: JSON.parse(JSON.stringify(this.credentialRequestOptions)),
+					optionsJSON: this.credentialRequestOptions,
 				})
 			} catch (error) {
 				switch (error.name) {
Index: src/main-challenge.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main-challenge.js b/src/main-challenge.js
--- a/src/main-challenge.js	(revision 1ef69d077f361d17126dec3c295d86107e1fff1f)
+++ b/src/main-challenge.js	(date 1787519368239)
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-or-later
  */
 
-import { createApp } from 'vue'
+import { createApp, markRaw } from 'vue'
 import { createPinia } from 'pinia'
 import Nextcloud from './mixins/Nextcloud.js'
 import Challenge from './components/Challenge.vue'
@@ -14,9 +14,7 @@
 
 const credentialRequestOptions = loadState('twofactor_webauthn', 'credential-request-options')
 const mainStore = useMainStore(pinia)
-mainStore.$patch({
-	credentialRequestOptions,
-})
+mainStore.credentialRequestOptions = markRaw(credentialRequestOptions)
 
 const app = createApp(Challenge)
 app.mixin(Nextcloud)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2. developing Work in progress bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DataCloneError on postMessage during WebAuthn challenge when credential comes from a password-manager extension (Bitwarden)

2 participants