From c020af4c09591b7f166e404d5e32593ca2f1958f Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 19 Jun 2026 21:30:52 +0200 Subject: [PATCH] fix(webauthn): add security-key hint so passkey managers defer Even with authenticatorAttachment=cross-platform, Bitwarden's extension intercepted navigator.credentials.create and offered to save a passkey. Add the WebAuthn L3 'hints: [security-key]' to both registration and assertion options; browsers and passkey managers that honor hints step aside and let the hardware key prompt through. Injected into the client payload only (the session copy used for server validation is unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 10 ++++++++++ app/Services/WebauthnService.php | 15 +++++++++++++-- config/clusev.php | 2 +- tests/Feature/WebauthnOptionsTest.php | 3 +++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a31c47b..d3a601e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._ +## [0.9.22] - 2026-06-19 + +### Behoben +- **Passwortmanager (Bitwarden) kaperte die Security-Key-Registrierung.** Trotz `cross-platform` + fing Bitwardens Browser-Erweiterung `navigator.credentials.create` ab und bot „Passkey speichern" + an. Die Optionen enthalten jetzt zusätzlich den **WebAuthn-L3-Hint `hints: ['security-key']`** (bei + Registrierung und Login), den moderne Browser/Manager respektieren und daher beiseite treten — der + YubiKey wird direkt abgefragt. (Ältere Bitwarden-Versionen, die den Hint nicht kennen, kapern + evtl. weiter — dann in Bitwarden „Nach Passkeys fragen" für die Seite/global deaktivieren.) + ## [0.9.21] - 2026-06-19 ### Behoben diff --git a/app/Services/WebauthnService.php b/app/Services/WebauthnService.php index 120d493..51e86d8 100644 --- a/app/Services/WebauthnService.php +++ b/app/Services/WebauthnService.php @@ -100,7 +100,14 @@ class WebauthnService $json = $this->serializer()->serialize($options, 'json'); session(['webauthn.register' => $json]); - return json_decode($json, true); + // WebAuthn L3 `hints`: tell the client to prefer a roaming HARDWARE security key. Modern + // browsers + passkey managers (Bitwarden, 1Password, …) that honor hints then step ASIDE + // and let the YubiKey prompt through instead of hijacking the ceremony to "save a passkey". + // Injected into the client payload only (not the session copy used for server validation). + $result = json_decode($json, true); + $result['hints'] = ['security-key']; + + return $result; } public function verifyRegistration(User $user, array $response, string $name): WebauthnCredential @@ -149,7 +156,11 @@ class WebauthnService $json = $this->serializer()->serialize($options, 'json'); session(['webauthn.login' => $json]); - return json_decode($json, true); + // Same security-key hint on login, so the passkey manager doesn't intercept the assertion. + $result = json_decode($json, true); + $result['hints'] = ['security-key']; + + return $result; } /** Validate an assertion against the user's stored credential. Never throws into the login path. */ diff --git a/config/clusev.php b/config/clusev.php index 7dc84b8..e1aa91c 100644 --- a/config/clusev.php +++ b/config/clusev.php @@ -2,7 +2,7 @@ return [ // First tagged release is v0.1.0 (semantic, not -dev). - 'version' => '0.9.21', + 'version' => '0.9.22', // Deployed commit + branch. install.sh bakes these into .env from the host's .git (the prod // image ships no .git); the Versions page prefers them and falls back to a live .git read in diff --git a/tests/Feature/WebauthnOptionsTest.php b/tests/Feature/WebauthnOptionsTest.php index 6930c87..976a608 100644 --- a/tests/Feature/WebauthnOptionsTest.php +++ b/tests/Feature/WebauthnOptionsTest.php @@ -35,6 +35,8 @@ class WebauthnOptionsTest extends TestCase $this->assertSame('cross-platform', data_get($opts, 'authenticatorSelection.authenticatorAttachment')); $this->assertSame('discouraged', data_get($opts, 'authenticatorSelection.residentKey')); $this->assertSame('discouraged', data_get($opts, 'authenticatorSelection.userVerification')); + // WebAuthn L3 hint so passkey managers (Bitwarden) defer to the hardware key. + $this->assertSame(['security-key'], data_get($opts, 'hints')); } public function test_assertion_options_require_only_user_presence(): void @@ -44,6 +46,7 @@ class WebauthnOptionsTest extends TestCase // discouraged = a single touch on login, no PIN/biometric prompt. $this->assertSame('discouraged', data_get($opts, 'userVerification')); + $this->assertSame(['security-key'], data_get($opts, 'hints')); } public function test_assertion_options_list_user_credentials(): void