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..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 @@ -93,7 +93,39 @@ 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) { + // Check for 'TypeError' instead of relying on the exact error message. + if (error.name !== 'TypeError') { + 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 +147,40 @@ customElements.define('passkey-submit', class extends HTMLElement { this.internals.form.submit(); } + convertToBase64(o) { + if (!o) { + return undefined; + } + + // Normalize Array to Uint8Array + if (Array.isArray(o)) { + o = Uint8Array.from(o); + } + + // Normalize ArrayBuffer to Uint8Array + if (o instanceof ArrayBuffer) { + o = new Uint8Array(o); + } + + // Convert Uint8Array to base64 + if (o instanceof Uint8Array) { + 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 convert to base64 string"); + } + + // Convert base64 to base64url + 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);