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