From 14a08efe66029596cc88712d6ccc84a5a2a1e1c3 Mon Sep 17 00:00:00 2001 From: Maarten Balliauw Date: Fri, 25 Jul 2025 07:25:27 +0200 Subject: [PATCH 1/5] Passkeys - JSON.stringify() fallback for various password managers --- .../Account/Shared/PasskeySubmit.razor.js | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js index 55a83bcc7ba1..5dfeb99bb15d 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js @@ -93,7 +93,38 @@ customElements.define('passkey-submit', class extends HTMLElement { const formData = new FormData(); try { const credential = await this.obtainCredential(useConditionalMediation, signal); - const credentialJson = JSON.stringify(credential); + + let credentialJson = ""; + try { + credentialJson = JSON.stringify(credential); + } catch (error) { + if (error.message !== 'Illegal invocation') { + throw error; + } + + // Some password managers do not implement PublicKeyCredential.prototype.toJSON correctly, + // which is required for JSON.stringify() to work. + // e.g. https://www.1password.community/discussions/1password/typeerror-illegal-invocation-in-chrome-browser/47399 + // Try and serialize the credential to JSON manually. + credentialJson = JSON.stringify({ + authenticatorAttachment: credential.authenticatorAttachment, + clientExtensionResults: credential.getClientExtensionResults(), + id: credential.id, + rawId: this.convertToBase64(credential.rawId), + response: { + attestationObject: this.convertToBase64(credential.response.attestationObject), + authenticatorData: this.convertToBase64(credential.response.authenticatorData ?? credential.response.getAuthenticatorData?.() ?? undefined), + clientDataJSON: this.convertToBase64(credential.response.clientDataJSON), + publicKey: this.convertToBase64(credential.response.getPublicKey?.() ?? undefined), + publicKeyAlgorithm: credential.response.getPublicKeyAlgorithm?.() ?? undefined, + transports: credential.response.getTransports?.() ?? undefined, + signature: this.convertToBase64(credential.response.signature), + userHandle: this.convertToBase64(credential.response.userHandle), + }, + type: credential.type, + }); + } + formData.append(`${this.attrs.name}.CredentialJson`, credentialJson); } catch (error) { if (error.name === 'AbortError') { @@ -115,6 +146,42 @@ customElements.define('passkey-submit', class extends HTMLElement { this.internals.form.submit(); } + convertToBase64(o) { + if (!o) { + return undefined; + } + + // Array or ArrayBuffer to Uint8Array + if (Array.isArray(o)) { + o = Uint8Array.from(o); + } + + if (o instanceof ArrayBuffer) { + o = new Uint8Array(o); + } + + // Uint8Array to base64 + if (o instanceof Uint8Array) { + var str = ""; + var len = o.byteLength; + + for (var i = 0; i < len; i++) { + str += String.fromCharCode(o[i]); + } + o = window.btoa(str); + } + + if (typeof o !== "string") { + throw new Error("could not coerce to string"); + } + + // base64 to base64url + // NOTE: "=" at the end of challenge is optional, strip it off here + o = o.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, ""); + + return o; + } + async tryAutofillPasskey() { if (browserSupportsPasskeys && this.attrs.operation === 'Request' && await PublicKeyCredential.isConditionalMediationAvailable?.()) { await this.obtainAndSubmitCredential(/* useConditionalMediation */ true); From c9481a00fc224fbeea866b38d58b03c79e9b8cb3 Mon Sep 17 00:00:00 2001 From: Maarten Balliauw Date: Fri, 25 Jul 2025 07:34:48 +0200 Subject: [PATCH 2/5] Update src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Components/Account/Shared/PasskeySubmit.razor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js index 5dfeb99bb15d..640deeb2035a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js @@ -98,7 +98,8 @@ customElements.define('passkey-submit', class extends HTMLElement { try { credentialJson = JSON.stringify(credential); } catch (error) { - if (error.message !== 'Illegal invocation') { + // Check for 'TypeError' instead of relying on the exact error message. + if (error.name !== 'TypeError') { throw error; } From 9c2a23f34afe8cca3549b7e05fec9cfc4764cc49 Mon Sep 17 00:00:00 2001 From: Maarten Balliauw Date: Fri, 25 Jul 2025 07:34:54 +0200 Subject: [PATCH 3/5] Update src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Components/Account/Shared/PasskeySubmit.razor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js index 640deeb2035a..3b2d129b69a0 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js @@ -164,7 +164,7 @@ customElements.define('passkey-submit', class extends HTMLElement { // Uint8Array to base64 if (o instanceof Uint8Array) { var str = ""; - var len = o.byteLength; + const len = o.byteLength; for (var i = 0; i < len; i++) { str += String.fromCharCode(o[i]); From 574d149ca2560398e1929db69145de71a401b1d4 Mon Sep 17 00:00:00 2001 From: Maarten Balliauw Date: Fri, 25 Jul 2025 07:34:59 +0200 Subject: [PATCH 4/5] Update src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Components/Account/Shared/PasskeySubmit.razor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js index 3b2d129b69a0..2a6cd0fb393f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js @@ -166,7 +166,7 @@ customElements.define('passkey-submit', class extends HTMLElement { var str = ""; const len = o.byteLength; - for (var i = 0; i < len; i++) { + for (let i = 0; i < len; i++) { str += String.fromCharCode(o[i]); } o = window.btoa(str); From 0c35fc3a60b2d97e03d03d10713784885bdc0856 Mon Sep 17 00:00:00 2001 From: Maarten Balliauw Date: Fri, 25 Jul 2025 09:18:27 +0200 Subject: [PATCH 5/5] Update src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js --- .../Account/Shared/PasskeySubmit.razor.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js index 2a6cd0fb393f..de1b55ce8bb3 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js @@ -152,32 +152,30 @@ customElements.define('passkey-submit', class extends HTMLElement { return undefined; } - // Array or ArrayBuffer to Uint8Array + // Normalize Array to Uint8Array if (Array.isArray(o)) { o = Uint8Array.from(o); } - + + // Normalize ArrayBuffer to Uint8Array if (o instanceof ArrayBuffer) { o = new Uint8Array(o); } - // Uint8Array to base64 + // Convert Uint8Array to base64 if (o instanceof Uint8Array) { - var str = ""; - const len = o.byteLength; - - for (let i = 0; i < len; i++) { + let str = ''; + for (let i = 0; i < o.byteLength; i++) { str += String.fromCharCode(o[i]); } o = window.btoa(str); } - if (typeof o !== "string") { - throw new Error("could not coerce to string"); + if (typeof o !== 'string') { + throw new Error("Could not convert to base64 string"); } - // base64 to base64url - // NOTE: "=" at the end of challenge is optional, strip it off here + // Convert base64 to base64url o = o.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, ""); return o;