JanssenProject / JanssenProject/jans
feat(jans-fido2): non-enumerating authentication failures
- Dominant language
- Java
- Stars
- 648
- Forks
- 174
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 110
Description
**Is your feature request related to a problem? Please describe.**
The FIDO2 assertion (authentication) path reveals whether a given username is registered, which enables user/account enumeration. In [AssertionService.options()](vscode-webview://1iavmi8q367e5lfsbh39brd12ogai0an0frq17ipifmc9b97ikhv/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AssertionService.java#L152-L166), when a username has no registered passkeys the server throws KEYS_NOT_FOUND with the message "Can't find associated key(s). Username: " + username, whereas a registered user receives a normal options response containing allowCredentials. An attacker can therefore distinguish "user exists / has a passkey" from "user does not" by the response alone.
Two separate oracles exist today:
Content oracle: the KEYS_NOT_FOUND error (and the distinct "Couldn't find the key by PublicKeyId" error in verify()) differ from the success/normal responses in status and body.
Timing oracle: the no-keys branch throws early (line ~164), before the request is persisted and the finish/metric steps run, so the unknown-user path returns measurably faster than a valid one.
BSI TR-03188 "Passkey Server" requires (NRM-7/8, §13.4.7 / §14.6.2) that authentication failures MUST NOT reveal whether a user is registered — this is a MUST at all three assurance levels (normal/substantial/high). Michael flagged this in #service_fido as a likely quick fix.
**Describe the solution you'd like**
Normalize the assertion-options and assertion-verify failure paths so that "unknown user," "user has no passkeys," and "wrong/absent credential" are indistinguishable in status code, response body, and (best-effort) timing:
Return a single generic authentication-failure envelope for these cases instead of the specific KEYS_NOT_FOUND / Couldn't find the key by PublicKeyId messages, and stop echoing the username back in the error text.
Avoid the early-return timing gap — the no-credentials case should follow a response path with comparable work/latency to the valid case (e.g. continue to build/return a normal-looking options response, or add constant-time padding), rather than throwing before the persistence/finish steps.
Audit both paths — options() (lines ~152–166) and verify() (lines ~358–377) — for any remaining branch or log/message that leaks user existence.
Consider interaction with userAutoEnrollment: when auto-enrollment is on, an unknown user is treated differently, which must not reintroduce an oracle.
Acceptance: an unknown-user request and a known-user-with-wrong-credential request are indistinguishable in HTTP status, response body, and best-effort timing. Detailed reasons may still be logged server-side and recorded in metrics, but must not be returned to the client.
**Describe alternatives you've considered**
- **Leave detailed errors as-is** — rejected; it violates a BSI MUST and is a real enumeration vector.
- **Only change the message text, not the timing** — insufficient; the early-return timing gap remains a (weaker) oracle. Message normalization is the priority; timing hardening is best-effort.
- **Suppress errors entirely** — rejected; we still need server-side logging/metrics for operators and the existing fallback telemetry (recordPasskeyFallback), so the fix is client-facing normalization only.
**Additional context**
- Part of the BSI TR-03188 track; enforcement is all-levels and independent of, but complementary to, the per-RP policy anchor [#14516](https://github.com/JanssenProject/jans/issues/14516) (BSI-01). This can land on its own.
- Ties into the existing error-envelope work (ErrorResponseFactory, {status, errorMessage}) and AssertionErrorResponseType.KEYS_NOT_FOUND.
- Grounded touch points: [AssertionService.java](vscode-webview://1iavmi8q367e5lfsbh39brd12ogai0an0frq17ipifmc9b97ikhv/jans-fido2/server/src/main/java/io/jans/fido2/service/operation/AssertionService.java) options() and verify(); verify server-fips/ parity if shared code changes.
- Scope note: design/enhancement issue — no implementation until reviewed (issues-first gate).
Contributor guide
Assessment
This issue has not been assessed yet.