Skip to content

Instantly share code, notes, and snippets.

@rbrayb
Created December 7, 2025 00:51
Show Gist options
  • Select an option

  • Save rbrayb/193c1b43d0ce3846bd6c51ca0f084983 to your computer and use it in GitHub Desktop.

Select an option

Save rbrayb/193c1b43d0ce3846bd6c51ca0f084983 to your computer and use it in GitHub Desktop.
An issue with the .NET 10 passkey implementation and 1Password
const browserSupportsPasskeys =
typeof navigator.credentials !== 'undefined' &&
typeof window.PublicKeyCredential !== 'undefined' &&
typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&
typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function';
// Passkey fix: Helper function to convert ArrayBuffer to base64url string
// Base64url encoding is required by WebAuthn spec - it's base64 but URL-safe
// (replaces + with -, / with _, and removes padding =)
function arrayBufferToBase64Url(buffer) {
if (!buffer) return undefined;
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = btoa(binary);
// Convert to base64url: replace + with -, / with _, and remove padding =
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
async function fetchWithErrorHandling(url, options = {}) {
const response = await fetch(url, {
credentials: 'include',
...options
});
if (!response.ok) {
const text = await response.text();
console.error(text);
throw new Error(`The server responded with status ${response.status}.`);
}
return response;
}
async function createCredential(headers, signal) {
const optionsResponse = await fetchWithErrorHandling('/Account/PasskeyCreationOptions', {
method: 'POST',
headers,
signal,
});
const optionsJson = await optionsResponse.json();
const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
return await navigator.credentials.create({ publicKey: options, signal });
}
async function requestCredential(email, mediation, headers, signal) {
const optionsResponse = await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${email}`, {
method: 'POST',
headers,
signal,
});
const optionsJson = await optionsResponse.json();
const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
return await navigator.credentials.get({ publicKey: options, mediation, signal });
}
customElements.define('passkey-submit', class extends HTMLElement {
static formAssociated = true;
connectedCallback() {
this.internals = this.attachInternals();
this.attrs = {
operation: this.getAttribute('operation'),
name: this.getAttribute('name'),
emailName: this.getAttribute('email-name'),
requestTokenName: this.getAttribute('request-token-name'),
requestTokenValue: this.getAttribute('request-token-value'),
};
this.internals.form.addEventListener('submit', (event) => {
if (event.submitter?.name === '__passkeySubmit') {
event.preventDefault();
this.obtainAndSubmitCredential();
}
});
this.tryAutofillPasskey();
}
disconnectedCallback() {
this.abortController?.abort();
}
async obtainCredential(useConditionalMediation, signal) {
if (!browserSupportsPasskeys) {
throw new Error('Some passkey features are missing. Please update your browser.');
}
const headers = {
[this.attrs.requestTokenName]: this.attrs.requestTokenValue,
};
if (this.attrs.operation === 'Create') {
return await createCredential(headers, signal);
} else if (this.attrs.operation === 'Request') {
const email = new FormData(this.internals.form).get(this.attrs.emailName);
const mediation = useConditionalMediation ? 'conditional' : undefined;
return await requestCredential(email, mediation, headers, signal);
} else {
throw new Error(`Unknown passkey operation '${this.attrs.operation}'.`);
}
}
async obtainAndSubmitCredential(useConditionalMediation = false) {
this.abortController?.abort();
this.abortController = new AbortController();
const signal = this.abortController.signal;
const formData = new FormData();
try {
const credential = await this.obtainCredential(useConditionalMediation, signal);
// Passkey fix: Manually convert ArrayBuffer fields to base64url strings
// credential.toJSON() is not available in all browsers/authenticators (like 1Password)
// This ensures proper base64url encoding as required by the WebAuthn spec
let credentialData;
if (typeof credential.toJSON === 'function') {
// Use toJSON if available
credentialData = credential.toJSON();
} else {
// Manually convert ArrayBuffer fields to base64url
credentialData = {
id: credential.id,
rawId: arrayBufferToBase64Url(credential.rawId),
type: credential.type,
response: {},
// Passkey fix: clientExtensionResults is required by server-side deserialization
// Always include it, even if empty, to avoid JSON deserialization errors
clientExtensionResults: credential.clientExtensionResults || {}
};
if (credential.response.attestationObject) {
// For credential creation (registration)
credentialData.response = {
attestationObject: arrayBufferToBase64Url(credential.response.attestationObject),
clientDataJSON: arrayBufferToBase64Url(credential.response.clientDataJSON)
};
if (credential.response.transports) {
credentialData.response.transports = credential.response.transports;
}
if (credential.response.publicKey) {
credentialData.response.publicKey = arrayBufferToBase64Url(credential.response.publicKey);
}
if (credential.response.publicKeyAlgorithm !== undefined) {
credentialData.response.publicKeyAlgorithm = credential.response.publicKeyAlgorithm;
}
if (credential.response.authenticatorData) {
credentialData.response.authenticatorData = arrayBufferToBase64Url(credential.response.authenticatorData);
}
} else if (credential.response.authenticatorData) {
// For credential assertion (login)
credentialData.response = {
authenticatorData: arrayBufferToBase64Url(credential.response.authenticatorData),
clientDataJSON: arrayBufferToBase64Url(credential.response.clientDataJSON),
signature: arrayBufferToBase64Url(credential.response.signature)
};
if (credential.response.userHandle) {
credentialData.response.userHandle = arrayBufferToBase64Url(credential.response.userHandle);
}
}
if (credential.authenticatorAttachment) {
credentialData.authenticatorAttachment = credential.authenticatorAttachment;
}
}
const credentialJson = JSON.stringify(credentialData);
formData.append(`${this.attrs.name}.CredentialJson`, credentialJson);
} catch (error) {
if (error.name === 'AbortError') {
// The user explicitly canceled the operation - return without error.
return;
}
console.error(error);
if (useConditionalMediation) {
// An error occurred during conditional mediation, which is not user-initiated.
// We log the error in the console but do not relay it to the user.
return;
}
const errorMessage = error.name === 'NotAllowedError'
? 'No passkey was provided by the authenticator.'
: error.message;
formData.append(`${this.attrs.name}.Error`, errorMessage);
}
this.internals.setFormValue(formData);
this.internals.form.submit();
}
async tryAutofillPasskey() {
if (browserSupportsPasskeys && this.attrs.operation === 'Request' && await PublicKeyCredential.isConditionalMediationAvailable?.()) {
await this.obtainAndSubmitCredential(/* useConditionalMediation */ true);
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment