Skip to content

Honour and verify the WebAuthn user handle (user.id) - #764

Open
jnbdz wants to merge 1 commit into
eclipse-vertx:masterfrom
SiteNetSoft:issue-580-581-webauthn-user-id
Open

Honour and verify the WebAuthn user handle (user.id)#764
jnbdz wants to merge 1 commit into
eclipse-vertx:masterfrom
SiteNetSoft:issue-580-581-webauthn-user-id

Conversation

@jnbdz

@jnbdz jnbdz commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #580
Fixes #581

Both issues were filed against the old vertx-auth-webauthn module, but the behaviour carried over to vertx-auth-webauthn4j: WebAuthn4JImpl.createCredentialsOptions() ignored the id of the user object and always put a random UUID in user.id, even though the docs/example describe id as the caller-provided base64url user handle. As a result authenticators cannot recognise an existing account and create duplicate credentials for the same user (#580), and there is no stable id to key authenticators on or to check the assertion userHandle against (#581).

#580user.id is honoured

  • createCredentialsOptions(user) uses user.id when present; it is validated as a base64url string of 1..64 bytes (the spec's user handle definition) and rejected with a clear error otherwise. When absent, a random UUID is generated as before.

#581 — the user handle is carried through the flow

  • Authenticator gains userId (base64url). It is stored via the existing CredentialStorage.storeCredential and, since the principal is authenticator.toJson(), exposed as userId in the authenticated User's principal.
  • WebAuthn4JCredentials gains userId:
    • webauthn.create: the relying party passes the user.id it sent to the browser (same way it passes username); it is stored on the new authenticator.
    • webauthn.get: optional; when the relying party identified the user before the ceremony, the credential must belong to that user.
  • webauthn.get also verifies that the assertion's response.userHandle, when non-empty, matches the stored userId of the credential (verifying an authentication assertion, step 6). Comparison is on decoded bytes so padded/unpadded encodings are equivalent.

Compatibility

  • Authenticators stored before this change have userId == null and are not subject to the check, so existing deployments keep working unchanged; empty user handles (non-discoverable credentials) are ignored.
  • CredentialStorage is not changed (it is @VertxGen and implemented by users). Implementations that want to index by user id can do so from the Authenticator handed to storeCredential, and for discoverable-credential logins (find(null, credentialId)) the returned Authenticator now carries the owner's userId. This deviates from the issue's suggestion of adding the id to the storage query, deliberately, to avoid a breaking interface change — happy to revisit if you'd rather extend the interface.
  • Generated converters were regenerated (AuthenticatorConverter, WebAuthn4JCredentialsConverter).

Tests

UserHandleTest (13 tests, offline): given/generated/invalid (non-base64url, empty, >64 bytes) user.id; registration stores the id and exposes it in the principal (with and without id); login with matching / mismatching userHandle, matching / mismatching expected userId, legacy authenticator without userId, and empty userHandle. Existing suite unchanged (the only local failure was EmulatorTest.testMetadata, which hits the live FIDO MDS endpoint and was returning HTTP 429 to my IP — same on master).

Docs: registration section of the asciidoc, WebAuthn4J#createCredentialsOptions javadoc and the examples updated to describe the user handle.

Fixes eclipse-vertx#580
Fixes eclipse-vertx#581

createCredentialsOptions() ignored the id of the given user object and
always generated a random UUID as user.id, so authenticators could not
recognise an existing account and happily created duplicate credentials
for the same user. The id is now used when present (validated as a
base64url string of 1..64 bytes, per the spec's user handle definition)
and only generated when absent.

Since the user handle is the stable, non user identifiable, key of an
account, it is now carried through the whole flow:

- Authenticator gains a userId property (stored by CredentialStorage
  along with the rest of the record and therefore part of the principal
  of the authenticated User);
- WebAuthn4JCredentials gains a userId property: at registration it is
  the user.id that was sent to the browser and is stored on the new
  authenticator; at authentication it is optional and, when set, the
  credential must belong to that user;
- at authentication the userHandle returned in the assertion is checked
  against the stored userId of the credential (verifying-assertion step
  6 of the spec). Authenticators registered before this change have no
  userId and are not subject to the check, so existing deployments keep
  working; empty user handles (non discoverable credentials) are ignored.

The CredentialStorage interface is unchanged: implementations that want
to index by user id can do so from the Authenticator handed to
storeCredential.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WebAuthn: Add userId property to Authenticator and WebAuthnCredentials WebAuthn: createCredentialOptions() uses a random UUID as user handle always

1 participant