JanssenProject / JanssenProject/jans

feat(jans-fido2): record ceremonies abandoned client-side so metrics reflect real outcomes

Open
#14,648 2 comments 1 reaction 1 assignee Claimed by @imran-ishaq View on GitHub
comp-jans-fido2 kind-feature
Dominant language
Java
Stars
647
Forks
173
Avg merge
1d 18h
Merged PRs (30d)
110

Description

**Is your feature request related to a problem? Please describe.**
FIDO2 metrics can never record a failed registration or authentication that fails on the client. A FAILURE row is written only from the catch block of verify() — AttestationService.verify() (AttestationService.java:395-397 → recordRegistrationFailureMetrics, :635) and AssertionService.verify() (AssertionService.java:439-441 → :516). Those blocks only run if a request actually reaches /attestation/result or /assertion/result.

The most common real-world failures never get that far. When a user fails biometric verification, cancels the OS prompt, or lets the ceremony time out, the authenticator rejects locally, the browser rejects the WebAuthn promise, and no /result call is made. The server has already written an ATTEMPT row at /options (AttestationService.java:225, AssertionService.java:222) and then hears nothing more.

Result: the metrics store only ever contains ATTEMPT and SUCCESS. In a live sample, all 61 entries were ATTEMPT or SUCCESS and zero were FAILURE, despite repeated deliberate biometric failures during testing. jansFido2MetricsErrorReason and jansFido2MetricsErrorCategory are empty on every row, so errorCounts in the aggregation is {} and getAttestationRejectionAnalysis has nothing to break down.

**Describe the solution you'd like**
Give an incomplete ceremony an explicit terminal outcome instead of leaving it as a dangling ATTEMPT:

Add a distinct status (e.g. ABANDONED) to Fido2MetricsConstants alongside ATTEMPT / SUCCESS / FAILURE, so "never completed" is not conflated with "server rejected it".
Close out expired ceremonies server-side. The pending Fido2RegistrationEntry / Fido2AuthenticationEntry already carries an expiration (unfinishedRequestExpiration, set at AttestationService.java:213-214 and AssertionService.java:211-212), so a sweep or expiry hook can find ceremonies that were started and never consumed and write one terminal row for each.
Optionally accept a client-reported outcome so the actual cause is captured (NotAllowedError, user cancel, timeout) rather than just "didn't finish". This should be treated as a hint only — never as the sole source of truth — since it is unauthenticated client input.

**Describe alternatives you've considered**

Derive abandonment at query time (attempts − successes − failures) without storing new rows. Cheapest, and it fixes the rates, but it stores no reason or timestamp, so it can't answer "why are users dropping off" and can't feed errorCounts.
Client-reported only, no server sweep. Richer reasons, but nothing is recorded when the browser tab is simply closed, and the counts become client-trust dependent.
Leave as-is and document it. Rejected — the aggregation already publishes these as registrationFailures / authenticationFailures, so consumers are being given numbers that don't mean what they're named.

**Additional context**
Depends on / pairs with the aggregation double-count issue. Even after that one is fixed, failures will read ~0 until this is addressed, because there are no FAILURE rows to count.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.