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) <noreply@anthropic.com>feat/v1-foundation v0.9.22
parent
5ebee9b4a5
commit
c020af4c09
10
CHANGELOG.md
10
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
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue