diff --git a/app/Actions/Passkeys/GeneratePasskeyRegisterOptionsAction.php b/app/Actions/Passkeys/GeneratePasskeyRegisterOptionsAction.php index 3a387b45f..fa9a09457 100644 --- a/app/Actions/Passkeys/GeneratePasskeyRegisterOptionsAction.php +++ b/app/Actions/Passkeys/GeneratePasskeyRegisterOptionsAction.php @@ -8,11 +8,25 @@ use App\Support\Passkeys\RelyingPartyIdResolver; use Spatie\LaravelPasskeys\Actions\GeneratePasskeyRegisterOptionsAction as BaseGeneratePasskeyRegisterOptionsAction; use Spatie\LaravelPasskeys\Models\Concerns\HasPasskeys; use Spatie\LaravelPasskeys\Support\Config; +use Webauthn\AuthenticatorSelectionCriteria; use Webauthn\PublicKeyCredentialCreationOptions; use Webauthn\PublicKeyCredentialRpEntity; final class GeneratePasskeyRegisterOptionsAction extends BaseGeneratePasskeyRegisterOptionsAction { + /** + * WebAuthn algorithms we tell the browser we accept. Order matters: + * the authenticator will pick the first algorithm it supports. + * -7 = ES256 (used by most FIDO2 hardware keys, Apple, Android) + * -257 = RS256 (used by Windows Hello / TPM-backed platform authenticators) + * -8 = EdDSA (used by some modern security keys & password managers) + */ + private const SUPPORTED_ALGORITHMS = [ + ['type' => 'public-key', 'alg' => -7], + ['type' => 'public-key', 'alg' => -257], + ['type' => 'public-key', 'alg' => -8], + ]; + public function execute( HasPasskeys $authenticatable, bool $asJson = true, @@ -28,24 +42,88 @@ final class GeneratePasskeyRegisterOptionsAction extends BaseGeneratePasskeyRegi return $options; } - $supportedAlgorithms = [ - ['type' => 'public-key', 'alg' => -7], // ES256 - ['type' => 'public-key', 'alg' => -257], // RS256 - ]; - // Different serializer/browser integrations can use either shape: // - options.pubKeyCredParams (WebAuthn JSON) // - options.publicKey.pubKeyCredParams (navigator.credentials.create payload) - // Enforce valid algorithms for both to prevent "alg undefined" errors. - $decoded['pubKeyCredParams'] = $supportedAlgorithms; + // Enforce valid algorithms (including RS256 for Windows Hello) for both + // to prevent "alg undefined" errors and to allow Windows TPM-backed + // platform authenticators to participate. + $decoded['pubKeyCredParams'] = self::SUPPORTED_ALGORITHMS; + + // WebAuthn L3 hints help the browser show a richer chooser including + // Windows Hello (client-device), phones (hybrid) and security keys. + $hints = array_values(array_filter((array) config('passkeys.hints', []))); + if ($hints !== []) { + $decoded['hints'] = $hints; + } + + // Request the credProps extension so we know whether a discoverable + // (resident) key was actually created by the authenticator. + if ((bool) config('passkeys.request_cred_props_extension', true)) { + $decoded['extensions'] = array_merge( + (array) ($decoded['extensions'] ?? []), + ['credProps' => true], + ); + } if (isset($decoded['publicKey']) && is_array($decoded['publicKey'])) { - $decoded['publicKey']['pubKeyCredParams'] = $supportedAlgorithms; + $decoded['publicKey']['pubKeyCredParams'] = self::SUPPORTED_ALGORITHMS; + + if ($hints !== []) { + $decoded['publicKey']['hints'] = $hints; + } + + if ((bool) config('passkeys.request_cred_props_extension', true)) { + $decoded['publicKey']['extensions'] = array_merge( + (array) ($decoded['publicKey']['extensions'] ?? []), + ['credProps' => true], + ); + } } return json_encode($decoded, JSON_THROW_ON_ERROR); } + /** + * Override the default selection criteria so that: + * - both platform authenticators (Windows Hello, Touch ID, password + * managers) and roaming/cross-platform FIDO2 security keys are offered; + * - a resident key is "preferred" rather than "required" — Windows + * domain-joined machines frequently refuse to expose Windows Hello when + * `required` is requested, which is why those users only saw the + * hardware-key dialog. + */ + public function authenticatorSelection(): AuthenticatorSelectionCriteria + { + $attachment = config('passkeys.authenticator_selection.authenticator_attachment'); + $userVerification = (string) config( + 'passkeys.authenticator_selection.user_verification', + AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED, + ); + $residentKey = config( + 'passkeys.authenticator_selection.resident_key', + AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_PREFERRED, + ); + + if (! in_array($attachment, AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENTS, true)) { + $attachment = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE; + } + + if (! in_array($userVerification, AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENTS, true)) { + $userVerification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED; + } + + if (! in_array($residentKey, AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENTS, true)) { + $residentKey = AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_PREFERRED; + } + + return new AuthenticatorSelectionCriteria( + authenticatorAttachment: $attachment, + userVerification: $userVerification, + residentKey: $residentKey, + ); + } + protected function relatedPartyEntity(): PublicKeyCredentialRpEntity { $rpId = RelyingPartyIdResolver::resolve(); diff --git a/config/passkeys.php b/config/passkeys.php index 35084080d..6fe55f327 100644 --- a/config/passkeys.php +++ b/config/passkeys.php @@ -42,6 +42,50 @@ return [ 'icon' => null, ], + /* + * Controls the WebAuthn `authenticatorSelection` ceremony parameters that are + * sent to the browser when a user is registering a new passkey. + * + * - `authenticator_attachment` accepts: null (no preference - allows both + * Windows Hello / Touch ID / password managers AND roaming FIDO2 keys), + * "platform" (Windows Hello / Touch ID / Android only) or + * "cross-platform" (only roaming/hardware security keys). + * Leave it `null` so users on Windows domain machines see Windows Hello, + * password managers AND hardware keys in the browser picker. + * + * - `resident_key` accepts: "preferred" (recommended), "required" or + * "discouraged". Some locked-down Windows domain machines refuse to expose + * the platform authenticator when "required" is requested. Use + * "preferred" for the widest interoperability. + * + * - `user_verification` accepts: "preferred", "required" or "discouraged". + */ + 'authenticator_selection' => [ + 'authenticator_attachment' => env('PASSKEY_AUTHENTICATOR_ATTACHMENT'), // null = no preference + 'resident_key' => env('PASSKEY_RESIDENT_KEY', 'preferred'), + 'user_verification' => env('PASSKEY_USER_VERIFICATION', 'preferred'), + ], + + /* + * WebAuthn Level 3 client hints. Modern Chromium based browsers (including + * Edge on Windows) use these to render a richer credential chooser that + * lists Windows Hello, mobile (hybrid/QR) and security keys side by side. + * + * Allowed values: "client-device", "hybrid", "security-key". + */ + 'hints' => [ + 'client-device', + 'hybrid', + 'security-key', + ], + + /* + * Whether to request the `credProps` WebAuthn extension. It tells the + * server (via the browser response) whether the created credential is a + * discoverable / resident key. Safe to leave enabled. + */ + 'request_cred_props_extension' => true, + /* * The models used by the package. * diff --git a/resources/views/partials/passkeys-manage.blade.php b/resources/views/partials/passkeys-manage.blade.php index 0b6ba1f53..1926cec10 100644 --- a/resources/views/partials/passkeys-manage.blade.php +++ b/resources/views/partials/passkeys-manage.blade.php @@ -18,10 +18,10 @@ data-passkeys='@json($passkeyPayload)' >

- Register a passkey from your security key, browser, or password manager. + Register a passkey using Windows Hello, Touch ID / Face ID, your phone, a password manager, or a FIDO2 security key.

- On managed/company devices, platform passkeys (Windows Hello/PIN) may be disabled by policy. In that case, use a FIDO2 security key or another allowed passkey provider. + On a Windows domain / managed device the browser will show all available options. If Windows Hello is still disabled by group policy, you can fall back to a hardware security key or scan a QR code with your phone.