bitwarden / bitwarden/android

[PM-43133] Passkey creation options do not match the WebAuthn spec, breaking registration and dropping RP constraints

Open
#7,359 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
Kotlin
Stars
9.4k
Forks
1.1k
Avg merge
1d 14h
Merged PRs (30d)
67

Description

### Origin

Native Application (non-browser app)

### Web URL or App name

Any relying party that sends spec-compliant creation options. This is in the JSON parsing, so it is not specific to one site or app.

### Passkey Action

- [x] Creating new passkey (Registration)
- [ ] Signing in (Authentication)

### Build Information

Password Manager 2026.8.0 (21819) on device. Also reproduced against `main` at b99ad92 with the unit test runner.

### Additional Information

`PasskeyAttestationOptions` does not match `PublicKeyCredentialCreationOptionsJSON` from the WebAuthn spec in four places. One of them breaks registration outright, the other three drop data without any error.

**1. `authenticatorSelection` is treated as required.**

In the spec only `rp`, `user`, `challenge` and `pubKeyCredParams` are required. `authenticatorSelection` is optional. In `PasskeyAttestationOptions.kt` it is a non-nullable property with no default, so a request that leaves it out fails to deserialize:

```
kotlinx.serialization.MissingFieldException: Field 'authenticatorSelection' is required
for type with serial name 'PasskeyAttestationOptions', but it was missing at path: $
```

`explicitNulls = false` does not help here because it only relaxes nullable properties, and `coerceInputValues = true` only rewrites values that are present but invalid.

Once the parse fails, `getPasskeyAttestationOptionsOrNull` returns null and registration ends in `Fido2RegisterCredentialResult.Error.MissingHostUrl` for an unprivileged caller, or `Error.InternalError` for a privileged one. The add item screen also falls back to the package name and an empty username.

**2. `excludeCredentials` is spelled `excludedCredentials`.**

`@SerialName("excludedCredentials")` never matches, so the exclusion list from the relying party is silently replaced with an empty list. `ignoreUnknownKeys = true` is what hides it.

This is not only a parsing detail. `registerFido2CredentialInternal` re-serializes the parsed model and sends that JSON to the SDK, so the exclusion list never reaches the authenticator and a duplicate passkey is not prevented.

Worth noting that the fixtures in this repo already use the spec name with an empty array, which is exactly why the tests never caught it:

- `RelyingPartyParserTest.kt:105` and `:139`
- `BitwardenCredentialManagerTest.kt:1520`

**3. `AuthenticatorAttachment.CROSS_PLATFORM` is mapped to `cross_platform`.**

The spec value has a hyphen. With `coerceInputValues = true` the unknown member is coerced to the property default, so `"authenticatorAttachment": "cross-platform"` decodes to null and then disappears from the JSON handed to the SDK.

**4. `ResidentKeyRequirement` is missing `discouraged`.**

The spec enum is `discouraged`, `preferred`, `required`. Only the last two are declared, so `"residentKey": "discouraged"` is coerced to null and dropped the same way.

I have a patch with unit tests ready and will open a PR against this issue.

Contributor guide

Open the contributing guide

Research direction

Start with PasskeyAttestationOptions.kt and the named RelyingPartyParserTest.kt and BitwardenCredentialManagerTest.kt fixtures; compare parsing and re-serialization with the WebAuthn creation-options names and enum values described in the issue. Run the cited unit tests, and consider the work done when optional input parses and all four values survive in the JSON sent to the SDK.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin
Domain
authentication, mobile
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.