writeLargeBlobMetadata() always reports written: false for credentials created by this package, because WebAuthnDIDProvider.createCredential() never asks for the largeBlob extension at registration.
Version 0.5.0.
Measured
Wrapping navigator.credentials in a consumer app (simple-todo) and creating a passkey through the documented create-or-recover flow:
1. create requested=[prf] largeBlob=null
2. get requested=[largeBlob] largeBlob={"written":false}
3. get requested=[prf] (keystore)
4. get requested=[] (signIdentity)
Call 2 is the write. It costs the user a biometric prompt and stores nothing.
Why
WebAuthn only permits writing a blob to a credential that was registered with largeBlob: { support: "preferred" | "required" }. In createCredential, the extension is added for the other two methods and not for this one:
if (keystoreEncryptionMethod === KEYSTORE_ENCRYPTION_METHODS.PRF) {
// adds prf
} else if (keystoreEncryptionMethod === KEYSTORE_ENCRYPTION_METHODS.HMAC_SECRET) {
// adds hmac-secret
}
// Note: largeBlob write happens after credential creation
The comment is accurate about when the write happens, but the registration half never arrives — so keystoreEncryptionMethod: 'largeBlob' cannot work either.
Why it went unnoticed
writeLargeBlobMetadata returns { assertion, extensionResults } and the outcome lives in extensionResults.largeBlob.written. The demo flow in examples/ — which consumers copy, as the TODO(upstream) in simple-todo's passkey-identity.js records — discards the return value and only catches exceptions. Nothing throws on a failed write, so it reads as success and silently falls through to the localStorage fallback.
The user-visible consequence is a passkey that does not survive a cleared browser profile, while the code and the copy both suggest it does.
Suggested
- Request
largeBlob: { support: 'preferred' } in createCredential — preferred rather than required so authenticators without it still register.
- Surface the registration result (
getClientExtensionResults().largeBlob.supported) on the returned credential info, so an app can offer the backup step only where it can succeed.
- Have
writeLargeBlobMetadata reject, or return an explicit boolean, when written is false — a silent no-op is the part that cost the diagnosis.
- Export the create-or-recover flow as an official helper. It exists only in
examples/, so every consumer copies these three calls along with this bug.
Consumer side, for reference
simple-todo removed the write rather than moving it behind an explicit button: a button offering to back the passkey up would fail in exactly the same way, and an action that asks for a fingerprint and achieves nothing silently is worse than no action. Creating a passkey there is now three prompts instead of four. The explicit backup step is worth adding once registration requests the extension.
writeLargeBlobMetadata()always reportswritten: falsefor credentials created by this package, becauseWebAuthnDIDProvider.createCredential()never asks for thelargeBlobextension at registration.Version 0.5.0.
Measured
Wrapping
navigator.credentialsin a consumer app (simple-todo) and creating a passkey through the documented create-or-recover flow:Call 2 is the write. It costs the user a biometric prompt and stores nothing.
Why
WebAuthn only permits writing a blob to a credential that was registered with
largeBlob: { support: "preferred" | "required" }. IncreateCredential, the extension is added for the other two methods and not for this one:The comment is accurate about when the write happens, but the registration half never arrives — so
keystoreEncryptionMethod: 'largeBlob'cannot work either.Why it went unnoticed
writeLargeBlobMetadatareturns{ assertion, extensionResults }and the outcome lives inextensionResults.largeBlob.written. The demo flow inexamples/— which consumers copy, as theTODO(upstream)in simple-todo'spasskey-identity.jsrecords — discards the return value and only catches exceptions. Nothing throws on a failed write, so it reads as success and silently falls through to the localStorage fallback.The user-visible consequence is a passkey that does not survive a cleared browser profile, while the code and the copy both suggest it does.
Suggested
largeBlob: { support: 'preferred' }increateCredential—preferredrather thanrequiredso authenticators without it still register.getClientExtensionResults().largeBlob.supported) on the returned credential info, so an app can offer the backup step only where it can succeed.writeLargeBlobMetadatareject, or return an explicit boolean, whenwrittenis false — a silent no-op is the part that cost the diagnosis.examples/, so every consumer copies these three calls along with this bug.Consumer side, for reference
simple-todo removed the write rather than moving it behind an explicit button: a button offering to back the passkey up would fail in exactly the same way, and an action that asks for a fingerprint and achieves nothing silently is worse than no action. Creating a passkey there is now three prompts instead of four. The explicit backup step is worth adding once registration requests the extension.