@@ -74,16 +74,22 @@ export class WebAuthnService {
7474 } as never ) ) as PublicKeyCredential ;
7575 }
7676
77+ if ( WebAuthnService . isInjectedCredential ( credential ) || ! credential . toJSON ) {
78+ return {
79+ response : JSON . stringify ( createResponseToJSON ( credential ) ) ,
80+ message : 'using fallback serializer (extension-injected credential)' ,
81+ } ;
82+ }
83+
7784 try {
78- console . log ( 'credential' , credential ) ;
7985 return {
8086 response : JSON . stringify ( credential . toJSON ( ) ) ,
8187 message : '' ,
8288 } ;
8389 } catch ( e ) {
8490 return {
8591 response : JSON . stringify ( createResponseToJSON ( credential ) ) ,
86- message : 'toJSON() not available on PublicKeyCredential' ,
92+ message : 'toJSON() threw on PublicKeyCredential' ,
8793 } ;
8894 }
8995 }
@@ -134,6 +140,13 @@ export class WebAuthnService {
134140 signal : abortController . signal ,
135141 } ) ) as PublicKeyCredential ;
136142
143+ if ( WebAuthnService . isInjectedCredential ( credential ) || ! credential . toJSON ) {
144+ return {
145+ response : JSON . stringify ( getResponseToJSON ( credential ) ) ,
146+ message : 'using fallback serializer (extension-injected credential)' ,
147+ } ;
148+ }
149+
137150 try {
138151 return {
139152 response : JSON . stringify ( credential . toJSON ( ) ) ,
@@ -142,7 +155,7 @@ export class WebAuthnService {
142155 } catch ( e ) {
143156 return {
144157 response : JSON . stringify ( getResponseToJSON ( credential ) ) ,
145- message : 'toJSON() not available on PublicKeyCredential' ,
158+ message : 'toJSON() threw on PublicKeyCredential' ,
146159 } ;
147160 }
148161 }
@@ -339,6 +352,32 @@ export class WebAuthnService {
339352 }
340353 }
341354
355+ /**
356+ * Detects whether a `PublicKeyCredential` was returned by a browser-extension
357+ * password manager (e.g. Dashlane, 1Password, Bitwarden) rather than the
358+ * browser's native WebAuthn implementation.
359+ *
360+ * Such extensions monkey-patch `navigator.credentials.get/create` and return
361+ * an object that lacks the WebAuthn internal slots. Calling the native
362+ * `PublicKeyCredential.prototype.toJSON()` on these objects either throws
363+ * `TypeError: Illegal invocation` or silently returns empty buffers
364+ * (`clientDataJSON`, `attestationObject`, `signature`, ...).
365+ *
366+ * Heuristic: real credentials inherit `getClientExtensionResults` from the
367+ * prototype; injected ones replace it with an own function.
368+ *
369+ * See https://github.com/bitwarden/clients/issues/12060.
370+ */
371+ static isInjectedCredential ( credential : PublicKeyCredential ) : boolean {
372+ try {
373+ return (
374+ credential . getClientExtensionResults !== PublicKeyCredential . prototype . getClientExtensionResults
375+ ) ;
376+ } catch ( e ) {
377+ return true ;
378+ }
379+ }
380+
342381 static async raceWithTimeout < T > ( p : Promise < T > , ms : number ) : Promise < T > {
343382 const timeout = new Promise < never > ( ( _ , reject ) =>
344383 setTimeout ( ( ) => reject ( new ConnectError ( ConnectErrorType . RaceTimeout , `timeout of ${ ms } ms reached` ) ) , ms ) ,
0 commit comments