Browse Source

config: advertise pm-2035-passkey-unlock feature flag

The bundled web vault gates its lock-screen "Unlock with passkey"
affordance on the `pm-2035-passkey-unlock` feature flag in
`/api/config`'s `featureStates`. Without it,
`WebAuthnPrfUnlockService.isPrfUnlockAvailable` short-circuits to
`false` and the button never renders even for users with a PRF-enabled
passkey enrolled.

Vaultwarden supports PRF passkey unlock end-to-end (the
`userDecryption.webAuthnPrfOptions` blob in `/api/sync` feeds the
client-side unwrap), so the flag must be advertised as enabled.

Extracts `build_feature_states` from the `/api/config` handler so the
feature-state assembly is unit-testable without `CONFIG` initialisation,
and pins both the new flag and the existing
`pm-19148-innovation-archive` companion via tests in `src/api/core/mod.rs`
+ a `/api/config` Playwright probe in `passkey.spec.ts`.
pull/7297/head
Zaid Marji 2 weeks ago
parent
commit
524194bbf4
  1. 19
      playwright/tests/passkey.spec.ts
  2. 69
      src/api/core/mod.rs

19
playwright/tests/passkey.spec.ts

@ -533,4 +533,23 @@ test.describe('UserDecryption response shapes match upstream Bitwarden', () => {
expect(Array.isArray(sync.userDecryption.webAuthnPrfOptions)).toBe(true);
expect(sync.userDecryption.webAuthnPrfOptions).toEqual([]);
});
test('/api/config advertises the pm-2035-passkey-unlock feature flag', async ({ request }) => {
// The lock-screen "Unlock with passkey" option is gated on the
// `pm-2035-passkey-unlock` feature flag in /api/config featureStates.
// The Bitwarden web vault's `WebAuthnPrfUnlockService.isPrfUnlockAvailable`
// short-circuits to `false` when the flag is missing or unset, hiding
// the button even when the user has a PRF-enabled passkey registered.
// Vaultwarden supports PRF passkey unlock end-to-end (the /sync
// `userDecryption.webAuthnPrfOptions` blob feeds the unwrap), so the
// flag must be advertised as enabled.
//
// Reference: `pm-2035-passkey-unlock` in
// https://github.com/bitwarden/clients/blob/main/libs/common/src/enums/feature-flag.enum.ts
const res = await request.get('/api/config');
expect(res.status()).toBe(200);
const config: any = await res.json();
expect(config.featureStates, 'featureStates must be an object').toBeTruthy();
expect(config.featureStates['pm-2035-passkey-unlock']).toBe(true);
});
});

69
src/api/core/mod.rs

@ -742,21 +742,70 @@ mod tests {
assert!(passkey_assertion_challenge_state(&bare, "any-token").is_err());
assert!(passkey_assertion_challenge_state(&bare, "").is_err());
}
/// `build_feature_states` must emit `pm-2035-passkey-unlock = true`
/// unconditionally — without it, the web vault's
/// `WebAuthnPrfUnlockService.isPrfUnlockAvailable` short-circuits to false
/// and the lock-screen "Unlock with passkey" option never renders even for
/// a user with a PRF-enabled passkey enrolled.
#[test]
fn feature_states_emits_passkey_unlock_flag_unconditionally() {
assert_eq!(build_feature_states("").get("pm-2035-passkey-unlock"), Some(&true));
assert_eq!(build_feature_states("some-unrelated-flag").get("pm-2035-passkey-unlock"), Some(&true));
}
/// `build_feature_states` must also emit `pm-19148-innovation-archive`
/// unconditionally — the existing companion flag the web vault expects
/// alongside the passkey-unlock entry.
#[test]
fn feature_states_emits_innovation_archive_flag_unconditionally() {
assert_eq!(build_feature_states("").get("pm-19148-innovation-archive"), Some(&true));
}
/// Valid experimental flags from the SUPPORTED list pass through; invalid
/// names are dropped (the `ValidOnly` filter). This pins the contract that
/// admin-configured `EXPERIMENTAL_CLIENT_FEATURE_FLAGS` reaches the wire.
#[test]
fn feature_states_passes_through_valid_experimental_flag() {
let probe = crate::config::SUPPORTED_FEATURE_FLAGS.iter().next().expect("at least one supported flag");
let states = build_feature_states(probe);
assert_eq!(states.get(*probe), Some(&true));
}
#[test]
fn feature_states_drops_unknown_experimental_flag() {
let states = build_feature_states("definitely-not-a-real-bitwarden-flag");
assert!(!states.contains_key("definitely-not-a-real-bitwarden-flag"));
}
}
/// Build the `featureStates` map returned by `/api/config`. Pure function
/// over the experimental-flags string so it can be exercised by unit tests
/// without `CONFIG` initialisation.
///
/// Official available feature flags can be found here:
/// Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135
/// Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12
/// Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31
/// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
fn build_feature_states(experimental_client_feature_flags: &str) -> std::collections::HashMap<String, bool> {
let mut feature_states =
parse_experimental_client_feature_flags(experimental_client_feature_flags, &FeatureFlagFilter::ValidOnly);
feature_states.insert("pm-19148-innovation-archive".to_owned(), true);
// Gates the web-vault's `Unlock with passkey` lock-screen option (and the
// matching desktop/mobile UI). `WebAuthnPrfUnlockService.isPrfUnlockAvailable`
// short-circuits to `false` when this flag is absent or unset, hiding the
// option even for users with a PRF-enabled passkey enrolled. Vaultwarden
// supports PRF-passkey unlock end-to-end via `userDecryption.webAuthnPrfOptions`
// on /sync, so the flag is advertised unconditionally.
feature_states.insert("pm-2035-passkey-unlock".to_owned(), true);
feature_states
}
#[get("/config")]
fn config() -> Json<Value> {
let domain = CONFIG.domain();
// Official available feature flags can be found here:
// Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135
// Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12
// Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31
// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
let mut feature_states = parse_experimental_client_feature_flags(
&CONFIG.experimental_client_feature_flags(),
&FeatureFlagFilter::ValidOnly,
);
feature_states.insert("pm-19148-innovation-archive".to_owned(), true);
let feature_states = build_feature_states(&CONFIG.experimental_client_feature_flags());
Json(json!({
// Note: The clients use this version to handle backwards compatibility concerns

Loading…
Cancel
Save