[PM-6471] Implement Inline Menu Autofill for Passkeys (#10127)

* PM-4661: Add passkey.username as item.username (#9756)

* Add incoming passkey.username as item.username

* Driveby fix, was sending wrong username

* added username to new-cipher too

* Guarded the if-block

* Update apps/browser/src/vault/popup/components/vault/add-edit.component.ts

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Fixed broken test

* fixed username on existing ciphers

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* PM-4878: Add passkey information to items when signing in (#9835)

* Added username to subtitle

* Added subName to cipher

* Moved subName to component

* Update apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts

Co-authored-by: SmithThe4th <gsmith@bitwarden.com>

* Fixed double code and added comment

* Added changeDetection: ChangeDetectionStrategy.OnPush as per review

---------

Co-authored-by: SmithThe4th <gsmith@bitwarden.com>

* [AC-2791] Members page - finish component library refactors (#9727)

* Replace PlatformUtilsService with ToastService

* Remove unneeded templates

* Implement table filtering function

* Move member-only methods from base class to subclass

* Move utility functions inside new MemberTableDataSource

* Rename PeopleComponent to MembersComponent

* [deps] Platform: Update angular-cli monorepo to v16.2.14 (#9380)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* [PM-8789] Move desktop_native into subcrate (#9682)

* Move desktop_native into subcrate

* Add publish = false to crates

* [PM-6394] remove policy evaluator cache (#9807)

* [PM-9364] Copy for Aggregate auto-scaling invoices for Teams and Enterprise customers (#9875)

* Change the seat adjustment message

* Move changes from en_GB file to en file

* revert changes in en_GB file

* Add feature flag to the change

* use user verification as a part of key rotation (#9722)

* Add the ability for custom validation logic to be injected into `UserVerificationDialogComponent` (#8770)

* Introduce `verificationType`

* Update template to use `verificationType`

* Implement a path for `verificationType = 'custom'`

* Delete `clientSideOnlyVerification`

* Update `EnrollMasterPasswordResetComponent` to include a server-side hash check

* Better describe the custom scenerio through comments

* Add an example of the custom verficiation scenerio

* Move execution of verification function into try/catch

* Migrate existing uses of `clientSideOnlyVerification`

* Use generic type option instead of casting

* Change "given" to "determined" in a comment

* Restructure the `org-redirect` guard to be Angular 17+ compliant (#9552)

* Document the `org-redirect` guard in code

* Make assertions about the way the `org-redirect` guard should behave

* Restructure the `org-redirect` guard to be Angular 17+ compliant

* Convert data parameter to function parameter

* Convert a data parameter to a function parameter that was missed

* Pass redirect function to default organization route

* don't initialize kdf with validators, do it on first set (#9754)

* add testids for attachments (#9892)

* Bug fix - error toast in 2fa (#9623)

* Bug fix - error toast in 2fa

* Bug fix - Yubikey code obscured

* 2FA error fix

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* [PM-2858] Fixing icon color

* [PM-2858] Adding subtitle for identity inline menu list items

* [PM-2858] Fixing jest tests

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through implementation of conditional identity fill logic on inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Working through identity field qualification for the inline menu

* [PM-2858] Scaffolding add new identity logic

* [PM-2858] Implementing add new identity

* [PM-2858] Implementing add new identity

* [PM-2858] Scaffolding add new identity logic

* [PM-2858] Scaffolding add new identity logic

* [PM-2858] Scaffolding add new identity logic

* [PM-2857] Fixing an issue with how we parse the last digits for credit card aria description

* [PM-2857] Setting up logic to ensrue we use a set email address as a fallback for a username

* [PM-2857] Fixing an issue with how we parse the last digits for credit card aria description

* [PM-2858] Reverting forced email address in inline menu identity autofill

* Restructure the `is-paid-org` guard to be Angular 17+ compliant (#9598)

* Document that `is-paid-org` guard in code

* Remove unused `MessagingService` dependency

* Make assertions about the way the is-paid-org guard should behave

* Restructure the `is-paid-org` guard to be Angular 17+ compliant

* Random commit to get the build job moving

* Undo previous commit

* Bumped client version(s) (#9895)

* [PM-9344] Clarify accepted user state (#9861)

* Prefer `Needs confirmation` to `Accepted` display status

This emphasizes that action is still required to complete setup.

* Remove unused message

* Bumped client version(s) (#9906)

* Revert "Bumped client version(s) (#9906)" (#9907)

This reverts commit 78c2829793.

* fix duo subscriptions and org vs individual duo setup (#9859)

* [PM-5024] Migrate tax-info component (#9872)

* Changes for the tax info migration

* Return for invalid formgroup

* Restructure the `org-permissions` guard to be Angular 17+ compliant (#9631)

* Document the `org-permissions` guard in code

* Restructure the `org-permissions` guard to be Angular 17+ compliant

* Update the `org-permissions` guard to use `ToastService`

* Simplify callback function sigantures

* Remove unused test object

* Fix updated route from merge

* Restructure the `provider-permissions` guard to be Angular 17+ compliant  (#9609)

* Document the `provider-permissions` guard in code

* Restructure the `provider-permissions` guard to be Angular 17+ compliant

* [deps] Platform: Update @types/argon2-browser to v1.18.4 (#8180)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bumped client version(s) (#9914)

* [PM-7162] Cipher Form - Item Details (#9758)

* [PM-7162] Fix weird angular error regarding disabled component bit-select

* [PM-7162] Introduce CipherFormConfigService and related types

* [PM-7162] Introduce CipherFormService

* [PM-7162] Introduce the Item Details section component and the CipherFormContainer interface

* [PM-7162] Introduce the CipherForm component

* [PM-7162] Add strongly typed QueryParams to the add-edit-v2.component

* [PM-7162] Export CipherForm from Vault Lib

* [PM-7162] Use the CipherForm in Browser AddEditV2

* [PM-7162] Introduce CipherForm storybook

* [PM-7162] Remove VaultPopupListFilterService dependency from NewItemDropDownV2 component

* [PM-7162] Add support for content projection of attachment button

* [PM-7162] Fix typo

* [PM-7162] Cipher form service cleanup

* [PM-7162] Move readonly collection notice to bit-hint

* [PM-7162] Refactor CipherFormConfig type to enforce required properties with Typescript

* [PM-7162] Fix storybook after config changes

* [PM-7162] Use new add-edit component for clone route

* [deps]: Update @yao-pkg/pkg to ^5.12.0 (#9820)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Autosync the updated translations (#9922)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9923)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9924)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* [AC-2830] Unable to create a free organization (#9917)

* Resolve the issue free org creation

* Check that the taxForm is touched

* [PM-7162] Fix broken getter when original cipher is null (#9927)

* [PM-8525] Edit Card (#9901)

* initial add of card details section

* add card number

* update card brand when the card number changes

* add year and month fields

* add security code field

* hide number and security code by default

* add `id` for all form fields

* update select options to match existing options

* make year input numerical

* only display card details for card ciphers

* use style to set input height

* handle numerical values for year

* update heading when a brand is available

* remove unused ref

* use cardview types for the form

* fix numerical input type

* disable card details when in partial-edit mode

* remove hardcoded height

* update types for formBuilder

* [PM-9440] Fix: handle undefined value in migration 66 (#9908)

* fix: handle undefined value in migration 66

* fix: the if-statement was typo

* Rename "encryptionAlgorithm" to "hashAlgorithmForEncryption" for clarity (#9891)

* [PM-7972] Account switching integration with "remember email" functionality (#9750)

* add account switching logic to login email service

* enforce boolean and fix desktop account switcher order

* [PM-9442] Add tests for undefined state values and proper emulation of ElectronStorageService in tests (#9910)

* fix: handle undefined value in migration 66

* fix: the if-statement was typo

* feat: duplicate error behavior in fake storage service

* feat: fix all migrations that were setting undefined values

* feat: add test for disabled fingrint in migration 66

* fix: default single user state saving undefined value to state

* revert: awaiting floating promise

gonna fix this in a separate PR

* Revert "feat: fix all migrations that were setting undefined values"

This reverts commit 034713256c.

* feat: automatically convert save to remove

* Revert "fix: default single user state saving undefined value to state"

This reverts commit 6c36da6ba5.

* [AC-2805] Consolidated Billing UI Updates (#9893)

* Add empty state for invoices

* Make cards on create client dialog tabbable

* Add space in $ / month per member

* Mute text, remove (Monthly) and right align menu on clients table

* Made used seats account for all users and fixed column sort for used/remaining

* Resize pricing cards

* Rename assignedSeats to occupiedSeats

* [PM-9460][deps] Tools: Update electron to v31 (#9921)

* [deps] Tools: Update electron to v31

* Bump version in electron-builder

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [AC-1452] Restrict access to 'Organization Info' and 'Two-Step Login' settings pages with a permission check (#9483)

* Guard Organization Info route - Owners only

* Guard TwoFactor route - Owners only and Organization must be able to use 2FA

* Update guards to use function syntax

---------

Co-authored-by: Addison Beck <hello@addisonbeck.com>

* [PM-9437] Use CollectionAccessDetailsResponse type now that is always the type returned from the API (#9951)

* Add required env variables to desktop native build script (#9869)

* [AC-2676] Remove paging logic from GroupsComponent (#9705)

* remove infinite scroll, use virtual scroll instead
* use TableDataSource for search
* allow sorting by name
* replacing PlatformUtilsService.showToast with ToastService
* misc FIXMEs

* [PM-9441] Catch and log exceptions during migration (#9905)

* feat: catch and log exceptions during migration

* Revert "feat: catch and log exceptions during migration"

This reverts commit d68733b7e5.

* feat: use log service to log migration errors

* Autosync the updated translations (#9972)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9973)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Updated codeowners for new design system team (#9913)

* Updated codeowners for new design system team.

* Moved Angular and Bootstrap dependencies

* Moved additional dependencies.

* Updated ownership

Co-authored-by: Will Martin <contact@willmartian.com>

---------

Co-authored-by: Will Martin <contact@willmartian.com>

* [SM-1016] Fix new access token dialog (#9918)

* swap to bit-dialog title & subtitle

* remove dialogRef.disableClose & use toastService

* Add shared two-factor-options component (#9767)

* Communicate the upcoming client vault privacy changes to MSPs (#9994)

* Add a banner notification to the provider portal

* Feature flag the banner

* Move banner copy to messages.json

* Allow for dismissing the banner

* Auth/PM-7321 - Registration with Email Verification - Registration Finish Component Implementation (#9653)

* PM-7321 - Temp add input password

* PM-7321 - update input password based on latest PR changes to test.

* PM-7321 - Progress on testing input password component + RegistrationFinishComponent checks

* PM-7321 - more progress on registration finish.

* PM-7321 - Wire up RegistrationFinishRequest model + AccountApiService abstraction + implementation changes for new method.

* PM-7321 - WIP Registration Finish - wiring up request building and API call on submit.

* PM-7321 - WIP registratin finish

* PM-7321 - WIP on creating registration-finish service + web override to add org invite handling

* PM-7321 - (1) Move web-registration-finish svc to web (2) Wire up exports (3) wire up RegistrationFinishComponent to call registration finish service

* PM-7321 - Get CLI building

* PM-7321 - Move all finish registration service and content to registration-finish feature folder.

* PM-7321 - Fix RegistrationFinishService config

* PM-7321 - RegistrationFinishComponent- handlePasswordFormSubmit - error handling WIP

* PM-7321 - InputPasswordComp - Update to accept masterPasswordPolicyOptions as input instead of retrieving it as parent components in different scenarios will need to retrieve the policies differently (e.g., orgInvite token in registration vs direct call via org id post SSO on set password)

* PM-7321 - Registration Finish - Add web specific logic for retrieving master password policies and passing them into the input password component.

* PM-7321 - Registration Start - Send email via query param to registration finish page so it can create masterKey

* PM-7321 - InputPassword comp - (1) Add loading input (2) Add email validation to submit logic.

* PM-7321 - Registration Finish - Add submitting state and pass into input password so that the rest of the registration process keeps the child form disabled.

* PM-7321 - Registration Finish - use validation service for error handling.

* PM-7321 - All register routes must be dynamic and change if the feature flag changes.

* PM-7321 - Test registration finish services.

* PM-7321 - RegisterRouteService - Add comment documenting why the service exists.

* PM-7321 - Add missing input password translations to browser & desktop

* PM-7321 - WebRegistrationFinishSvc - apply PR feedback

* [deps] Autofill: Update rimraf to v5.0.8 (#10008)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* [PM-9318] Fix username on protonpass import (#9889)

* Fix username field used for ProtonPass import

ProtonPass has changed their export format and userName is not itemEmail

* Import additional field itemUsername

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [PM-8943] Update QRious script initialization in Authenticator two-factor provider (#9926)

* create onload() for qrious as well as error messaging if QR code cannot be displayed

* button and message updates and formpromise removal

* load QR script async

* rename and reorder methods

* Delete Unused Bits of StateService (#9858)

* Delete Unused Bits of StateService

* Fix Tests

* remove getBgService for auth request service (#10020)

* [PM-2858] Fixing an issue found when the first or last names of an identity are not filled

* [PM-2858] Fixing an issue found where keyboard navigation can potentially close the inline menu

* [PM-2858] Fixing jest tests within inline menu list

* [PM-2858] Fixing jest tests within inline menu list

* [PM-2858] Setting up login items to be presented when an account creation form is shown to the user

* [PM-2858] Refactoring implementation used for creating the inline menu cipher data

* [PM-2858] Refactoring implementation used for creating the inline menu cipher data

* [PM-2858] Refactoring implementation used for creating the inline menu cipher data

* [PM-2858] Refactoring implementation

* [PM-2858] Refactoring implementation

* [PM-2858] Refactoring implementation

* [PM-2858] Refactoring implementation

* [PM-2858] Changing how we populate login ciphers within create account

* [PM-2858] Adding documentation

* [PM-2858] Working through jest tests for the OverlayBackground

* [PM-2858] Working through jest tests for the OverlayBackground

* [PM-2858] Working through jest tests for the AutofillInlineMenuList class

* [PM-2858] Adding documentation to inline menu list methods

* [PM-2857] Fixing a jest test

* [PM-2858] Fixing jest tests within inline menu list

* [PM-2858] Addressing jest tests within AutofillOverlayContentService

* [PM-2858] Addressing jest tests within AutofillOverlayContentService

* [PM-2858] Addressing jest tests within InlineMenuFieldQualificationService

* [PM-9267] Implement feature flag for inline menu re-architecture (#9845)

* [PM-9267] Implement Feature Flag for Inline Menu Re-Architecture

* [PM-9267] Incorporating legacy OverlayBackground implementation

* [PM-9267] Incorporating legacy overlay content scripts

* [PM-9267] Incorporating legacy overlay content scripts

* [PM-9267] Incorporating legacy overlay content scripts

* [PM-9267] Incorporating legacy overlay content scripts

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Finalizing feature flag implementation

* [PM-9267] Adjusting naming convention for page files

* [PM-9267] Adjusting naming convention for page files

* [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account

* PM-4950 - Fix hint and verify delete components that had the data in the wrong place (#9877)

* PM-4661: Add passkey.username as item.username (#9756)

* Add incoming passkey.username as item.username

* Driveby fix, was sending wrong username

* added username to new-cipher too

* Guarded the if-block

* Update apps/browser/src/vault/popup/components/vault/add-edit.component.ts

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Fixed broken test

* fixed username on existing ciphers

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* PM-4878: Add passkey information to items when signing in (#9835)

* Added username to subtitle

* Added subName to cipher

* Moved subName to component

* Update apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts

Co-authored-by: SmithThe4th <gsmith@bitwarden.com>

* Fixed double code and added comment

* Added changeDetection: ChangeDetectionStrategy.OnPush as per review

---------

Co-authored-by: SmithThe4th <gsmith@bitwarden.com>

* [AC-2791] Members page - finish component library refactors (#9727)

* Replace PlatformUtilsService with ToastService

* Remove unneeded templates

* Implement table filtering function

* Move member-only methods from base class to subclass

* Move utility functions inside new MemberTableDataSource

* Rename PeopleComponent to MembersComponent

* [deps] Platform: Update angular-cli monorepo to v16.2.14 (#9380)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* [PM-8789] Move desktop_native into subcrate (#9682)

* Move desktop_native into subcrate

* Add publish = false to crates

* [PM-6394] remove policy evaluator cache (#9807)

* [PM-9364] Copy for Aggregate auto-scaling invoices for Teams and Enterprise customers (#9875)

* Change the seat adjustment message

* Move changes from en_GB file to en file

* revert changes in en_GB file

* Add feature flag to the change

* use user verification as a part of key rotation (#9722)

* Add the ability for custom validation logic to be injected into `UserVerificationDialogComponent` (#8770)

* Introduce `verificationType`

* Update template to use `verificationType`

* Implement a path for `verificationType = 'custom'`

* Delete `clientSideOnlyVerification`

* Update `EnrollMasterPasswordResetComponent` to include a server-side hash check

* Better describe the custom scenerio through comments

* Add an example of the custom verficiation scenerio

* Move execution of verification function into try/catch

* Migrate existing uses of `clientSideOnlyVerification`

* Use generic type option instead of casting

* Change "given" to "determined" in a comment

* Restructure the `org-redirect` guard to be Angular 17+ compliant (#9552)

* Document the `org-redirect` guard in code

* Make assertions about the way the `org-redirect` guard should behave

* Restructure the `org-redirect` guard to be Angular 17+ compliant

* Convert data parameter to function parameter

* Convert a data parameter to a function parameter that was missed

* Pass redirect function to default organization route

* don't initialize kdf with validators, do it on first set (#9754)

* add testids for attachments (#9892)

* Bug fix - error toast in 2fa (#9623)

* Bug fix - error toast in 2fa

* Bug fix - Yubikey code obscured

* 2FA error fix

* Restructure the `is-paid-org` guard to be Angular 17+ compliant (#9598)

* Document that `is-paid-org` guard in code

* Remove unused `MessagingService` dependency

* Make assertions about the way the is-paid-org guard should behave

* Restructure the `is-paid-org` guard to be Angular 17+ compliant

* Random commit to get the build job moving

* Undo previous commit

* Bumped client version(s) (#9895)

* [PM-9344] Clarify accepted user state (#9861)

* Prefer `Needs confirmation` to `Accepted` display status

This emphasizes that action is still required to complete setup.

* Remove unused message

* Bumped client version(s) (#9906)

* Revert "Bumped client version(s) (#9906)" (#9907)

This reverts commit 78c2829793.

* fix duo subscriptions and org vs individual duo setup (#9859)

* [PM-5024] Migrate tax-info component (#9872)

* Changes for the tax info migration

* Return for invalid formgroup

* Restructure the `org-permissions` guard to be Angular 17+ compliant (#9631)

* Document the `org-permissions` guard in code

* Restructure the `org-permissions` guard to be Angular 17+ compliant

* Update the `org-permissions` guard to use `ToastService`

* Simplify callback function sigantures

* Remove unused test object

* Fix updated route from merge

* Restructure the `provider-permissions` guard to be Angular 17+ compliant  (#9609)

* Document the `provider-permissions` guard in code

* Restructure the `provider-permissions` guard to be Angular 17+ compliant

* [deps] Platform: Update @types/argon2-browser to v1.18.4 (#8180)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bumped client version(s) (#9914)

* [PM-7162] Cipher Form - Item Details (#9758)

* [PM-7162] Fix weird angular error regarding disabled component bit-select

* [PM-7162] Introduce CipherFormConfigService and related types

* [PM-7162] Introduce CipherFormService

* [PM-7162] Introduce the Item Details section component and the CipherFormContainer interface

* [PM-7162] Introduce the CipherForm component

* [PM-7162] Add strongly typed QueryParams to the add-edit-v2.component

* [PM-7162] Export CipherForm from Vault Lib

* [PM-7162] Use the CipherForm in Browser AddEditV2

* [PM-7162] Introduce CipherForm storybook

* [PM-7162] Remove VaultPopupListFilterService dependency from NewItemDropDownV2 component

* [PM-7162] Add support for content projection of attachment button

* [PM-7162] Fix typo

* [PM-7162] Cipher form service cleanup

* [PM-7162] Move readonly collection notice to bit-hint

* [PM-7162] Refactor CipherFormConfig type to enforce required properties with Typescript

* [PM-7162] Fix storybook after config changes

* [PM-7162] Use new add-edit component for clone route

* [deps]: Update @yao-pkg/pkg to ^5.12.0 (#9820)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Autosync the updated translations (#9922)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9923)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9924)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* [AC-2830] Unable to create a free organization (#9917)

* Resolve the issue free org creation

* Check that the taxForm is touched

* [PM-7162] Fix broken getter when original cipher is null (#9927)

* [PM-8525] Edit Card (#9901)

* initial add of card details section

* add card number

* update card brand when the card number changes

* add year and month fields

* add security code field

* hide number and security code by default

* add `id` for all form fields

* update select options to match existing options

* make year input numerical

* only display card details for card ciphers

* use style to set input height

* handle numerical values for year

* update heading when a brand is available

* remove unused ref

* use cardview types for the form

* fix numerical input type

* disable card details when in partial-edit mode

* remove hardcoded height

* update types for formBuilder

* [PM-9440] Fix: handle undefined value in migration 66 (#9908)

* fix: handle undefined value in migration 66

* fix: the if-statement was typo

* Rename "encryptionAlgorithm" to "hashAlgorithmForEncryption" for clarity (#9891)

* [PM-7972] Account switching integration with "remember email" functionality (#9750)

* add account switching logic to login email service

* enforce boolean and fix desktop account switcher order

* [PM-9442] Add tests for undefined state values and proper emulation of ElectronStorageService in tests (#9910)

* fix: handle undefined value in migration 66

* fix: the if-statement was typo

* feat: duplicate error behavior in fake storage service

* feat: fix all migrations that were setting undefined values

* feat: add test for disabled fingrint in migration 66

* fix: default single user state saving undefined value to state

* revert: awaiting floating promise

gonna fix this in a separate PR

* Revert "feat: fix all migrations that were setting undefined values"

This reverts commit 034713256c.

* feat: automatically convert save to remove

* Revert "fix: default single user state saving undefined value to state"

This reverts commit 6c36da6ba5.

* [AC-2805] Consolidated Billing UI Updates (#9893)

* Add empty state for invoices

* Make cards on create client dialog tabbable

* Add space in $ / month per member

* Mute text, remove (Monthly) and right align menu on clients table

* Made used seats account for all users and fixed column sort for used/remaining

* Resize pricing cards

* Rename assignedSeats to occupiedSeats

* [PM-9460][deps] Tools: Update electron to v31 (#9921)

* [deps] Tools: Update electron to v31

* Bump version in electron-builder

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [AC-1452] Restrict access to 'Organization Info' and 'Two-Step Login' settings pages with a permission check (#9483)

* Guard Organization Info route - Owners only

* Guard TwoFactor route - Owners only and Organization must be able to use 2FA

* Update guards to use function syntax

---------

Co-authored-by: Addison Beck <hello@addisonbeck.com>

* [PM-9437] Use CollectionAccessDetailsResponse type now that is always the type returned from the API (#9951)

* Add required env variables to desktop native build script (#9869)

* [AC-2676] Remove paging logic from GroupsComponent (#9705)

* remove infinite scroll, use virtual scroll instead
* use TableDataSource for search
* allow sorting by name
* replacing PlatformUtilsService.showToast with ToastService
* misc FIXMEs

* [PM-9441] Catch and log exceptions during migration (#9905)

* feat: catch and log exceptions during migration

* Revert "feat: catch and log exceptions during migration"

This reverts commit d68733b7e5.

* feat: use log service to log migration errors

* Autosync the updated translations (#9972)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Autosync the updated translations (#9973)

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>

* Updated codeowners for new design system team (#9913)

* Updated codeowners for new design system team.

* Moved Angular and Bootstrap dependencies

* Moved additional dependencies.

* Updated ownership

Co-authored-by: Will Martin <contact@willmartian.com>

---------

Co-authored-by: Will Martin <contact@willmartian.com>

* [SM-1016] Fix new access token dialog (#9918)

* swap to bit-dialog title & subtitle

* remove dialogRef.disableClose & use toastService

* Add shared two-factor-options component (#9767)

* Communicate the upcoming client vault privacy changes to MSPs (#9994)

* Add a banner notification to the provider portal

* Feature flag the banner

* Move banner copy to messages.json

* Allow for dismissing the banner

* Auth/PM-7321 - Registration with Email Verification - Registration Finish Component Implementation (#9653)

* PM-7321 - Temp add input password

* PM-7321 - update input password based on latest PR changes to test.

* PM-7321 - Progress on testing input password component + RegistrationFinishComponent checks

* PM-7321 - more progress on registration finish.

* PM-7321 - Wire up RegistrationFinishRequest model + AccountApiService abstraction + implementation changes for new method.

* PM-7321 - WIP Registration Finish - wiring up request building and API call on submit.

* PM-7321 - WIP registratin finish

* PM-7321 - WIP on creating registration-finish service + web override to add org invite handling

* PM-7321 - (1) Move web-registration-finish svc to web (2) Wire up exports (3) wire up RegistrationFinishComponent to call registration finish service

* PM-7321 - Get CLI building

* PM-7321 - Move all finish registration service and content to registration-finish feature folder.

* PM-7321 - Fix RegistrationFinishService config

* PM-7321 - RegistrationFinishComponent- handlePasswordFormSubmit - error handling WIP

* PM-7321 - InputPasswordComp - Update to accept masterPasswordPolicyOptions as input instead of retrieving it as parent components in different scenarios will need to retrieve the policies differently (e.g., orgInvite token in registration vs direct call via org id post SSO on set password)

* PM-7321 - Registration Finish - Add web specific logic for retrieving master password policies and passing them into the input password component.

* PM-7321 - Registration Start - Send email via query param to registration finish page so it can create masterKey

* PM-7321 - InputPassword comp - (1) Add loading input (2) Add email validation to submit logic.

* PM-7321 - Registration Finish - Add submitting state and pass into input password so that the rest of the registration process keeps the child form disabled.

* PM-7321 - Registration Finish - use validation service for error handling.

* PM-7321 - All register routes must be dynamic and change if the feature flag changes.

* PM-7321 - Test registration finish services.

* PM-7321 - RegisterRouteService - Add comment documenting why the service exists.

* PM-7321 - Add missing input password translations to browser & desktop

* PM-7321 - WebRegistrationFinishSvc - apply PR feedback

* [deps] Autofill: Update rimraf to v5.0.8 (#10008)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* [PM-9318] Fix username on protonpass import (#9889)

* Fix username field used for ProtonPass import

ProtonPass has changed their export format and userName is not itemEmail

* Import additional field itemUsername

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* [PM-8943] Update QRious script initialization in Authenticator two-factor provider (#9926)

* create onload() for qrious as well as error messaging if QR code cannot be displayed

* button and message updates and formpromise removal

* load QR script async

* rename and reorder methods

* Delete Unused Bits of StateService (#9858)

* Delete Unused Bits of StateService

* Fix Tests

* remove getBgService for auth request service (#10020)

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: SmithThe4th <gsmith@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
Co-authored-by:  Audrey  <ajensen@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Addison Beck <github@addisonbeck.com>
Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com>
Co-authored-by: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Opeyemi <Alaoopeyemi101@gmail.com>
Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Bernd Schoolmann <mail@quexten.com>
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
Co-authored-by: Addison Beck <hello@addisonbeck.com>
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
Co-authored-by: Will Martin <contact@willmartian.com>
Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com>

* [PM-1223] Passkeys Mediated Conditional UI

* [PM-1223] Passkeys Mediated Conditional UI

* [PM-1223] Finished migrating existing POC solution

* [PM-1223] Setting up passkeys to appear before login ciphers

* [PM-6471] Implement on-page autofill menu for passkeys

* [PM-6471] Working through visual presentation of passkeys within inline menu

* [PM-6471] Implementing visual and behavior differences between inline menu passkeys and regular login elements

* [PM-6471] Adding a11y content within inline menu list elements

* [PM-6471] Fixing issue with SVG path fill on new passkey icon

* [PM-6471] Working through scroll event triggers

* [PM-6471] Refactoring onScroll implementation

* [PM-6471] Adding a methodology for allow users to cancel a conditional UI workflow, but still be able to re-trigger the passkey fill

* [PM-2858] Fixing an issue found where password fields addedin new account forms do not properly pull their value into the add cipher flow

* [PM-6471] Implementing a methodology for exlusively displaying credentials that are authorized within the fido2 request

* [PM-6471] Adding the webAuthn autocomplete value to the field qualification service

* [PM-6471] Fixing issues within OverlayBackground jest tests

* [PM-6471] Fixing issues within AutofillInlineMenuList jest tests

* [PM-6471] Adding jest tests for the OverlayBackground

* [PM-6471] Adding jest tests for the OverlayBackground

* [PM-6471] Adding jest tests for the OverlayBackground

* [PM-6471] Re-adding an optimization to the inline menu list

* [PM-6471] Refactoring implementation, optimizing scroll behavior within the inline menu, and adding a method for ensureing passkeys get set as the most recently used cipher when fill occurs

* [PM-6471] Refactoring implementation, optimizing scroll behavior within the inline menu, and adding a method for ensureing passkeys get set as the most recently used cipher when fill occurs

* [PM-6471] Refactoring how we identify a cipher as a passkey cipher

* [PM-6471] Reworking implementation to loop mediated conditional request until a valid value is returned rather than re-calling navigator API

* [PM-6471] Adding jest tests for the inline menu list logic

* [PM-6471] Adding jest tests for the inline menu list logic

* [PM-6471] Adding jest tests for conditional mediated webauthn request

* [PM-6471] Removing unnecessary comment

* [PM-6471] Adding jest tests for incorporated Fido2ClientService changes

* [PM-6471] Adding jest tests to the Fido2AuthenticatorService changes

* [PM-6471] Adding jest tests for the Fido2ActiveRequestManager class

* [PM-6471] Fixing issue with master password reprompt not triggering for cipher when user verification is discouraged

* [PM-2858] Adjusting scrollbar stylings

* [PM-2858] Adjusting how we handle instantiating the feature flag guarded overlay background and how we handle instantiating identities and card ciphers in the inline menu

* [PM-2858] Adjusting how we handle instantiating the feature flag guarded overlay background and how we handle instantiating identities and card ciphers in the inline menu

* [PM-2858] Adjusting how we handle instantiating the feature flag guarded overlay background and how we handle instantiating identities and card ciphers in the inline menu

* [PM-2858] Incorporating some changes that ensure the inline menu list fades in as expected

* [PM-2858] Incorporating some changes that ensure the inline menu list fades in as expected

* [PM-2858] Incorporating some changes that ensure the inline menu list fades in as expected

* [PM-2858] Adjusting how we inject translations for a couple of aria label elements

* [PM-6471] Merging changes from identities branch

* [PM-6471] Fixing an issue relating to a current tab reference

* [PM-6471] Fixing an issue relating to a current tab reference

* [PM-6471] Optimizing conditional logic for OverlayBackground.showCipherAsPasskey

* [PM-6471] Refactoring implementation

* [PM-6471] Refactoring implementation

* [PM-6471] Adding coverage for cases where a mediated conditional request is aborted

* [PM-6471] Fixing typechecking error

---------

Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: SmithThe4th <gsmith@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
Co-authored-by:  Audrey  <ajensen@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Addison Beck <github@addisonbeck.com>
Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com>
Co-authored-by: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Opeyemi <Alaoopeyemi101@gmail.com>
Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Bernd Schoolmann <mail@quexten.com>
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
Co-authored-by: Addison Beck <hello@addisonbeck.com>
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
Co-authored-by: Will Martin <contact@willmartian.com>
Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com>
This commit is contained in:
Cesar Gonzalez 2024-08-07 16:25:19 -05:00 committed by GitHub
parent 1243cbd411
commit dfb69f8130
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1889 additions and 119 deletions

View File

@ -3926,6 +3926,18 @@
"data": {
"message": "Data"
},
"passkeys": {
"message": "Passkeys",
"description": "A section header for a list of passkeys. Used in the inline menu list."
},
"passwords": {
"message": "Passwords",
"description": "A section header for a list of passwords. Used in the inline menu list."
},
"logInWithPasskeyAriaLabel": {
"message": "Log in with passkey",
"description": "ARIA label for the inline menu button that logs in with a passkey."
},
"assign": {
"message": "Assign"
},

View File

@ -1,5 +1,6 @@
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import AutofillPageDetails from "../../models/autofill-page-details";
import { PageDetail } from "../../services/abstractions/autofill.service";
@ -132,6 +133,7 @@ export type OverlayPortMessage = {
direction?: string;
inlineMenuCipherId?: string;
addNewCipherType?: CipherType;
usePasskey?: boolean;
};
export type InlineMenuCipherData = {
@ -142,7 +144,13 @@ export type InlineMenuCipherData = {
favorite: boolean;
icon: WebsiteIconData;
accountCreationFieldType?: string;
login?: { username: string };
login?: {
username: string;
passkey: {
rpName: string;
userName: string;
} | null;
};
card?: string;
identity?: {
fullName: string;
@ -150,6 +158,15 @@ export type InlineMenuCipherData = {
};
};
export type BuildCipherDataParams = {
inlineMenuCipherId: string;
cipher: CipherView;
showFavicons?: boolean;
showInlineMenuAccountCreation?: boolean;
hasPasskey?: boolean;
identityData?: { fullName: string; username?: string };
};
export type BackgroundMessageParam = {
message: OverlayBackgroundExtensionMessage;
};

View File

@ -17,6 +17,7 @@ import {
EnvironmentService,
Region,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { Fido2ClientService } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
@ -32,6 +33,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
import { BrowserApi } from "../../platform/browser/browser-api";
import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service";
@ -85,6 +87,8 @@ describe("OverlayBackground", () => {
let autofillSettingsService: MockProxy<AutofillSettingsService>;
let i18nService: MockProxy<I18nService>;
let platformUtilsService: MockProxy<BrowserPlatformUtilsService>;
let availableAutofillCredentialsMock$: BehaviorSubject<Fido2CredentialView[]>;
let fido2ClientService: MockProxy<Fido2ClientService>;
let selectedThemeMock$: BehaviorSubject<ThemeType>;
let themeStateService: MockProxy<ThemeStateService>;
let overlayBackground: OverlayBackground;
@ -151,6 +155,10 @@ describe("OverlayBackground", () => {
autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$;
i18nService = mock<I18nService>();
platformUtilsService = mock<BrowserPlatformUtilsService>();
availableAutofillCredentialsMock$ = new BehaviorSubject([]);
fido2ClientService = mock<Fido2ClientService>({
availableAutofillCredentials$: (_tabId) => availableAutofillCredentialsMock$,
});
selectedThemeMock$ = new BehaviorSubject(ThemeType.Light);
themeStateService = mock<ThemeStateService>();
themeStateService.selectedTheme$ = selectedThemeMock$;
@ -164,6 +172,7 @@ describe("OverlayBackground", () => {
autofillSettingsService,
i18nService,
platformUtilsService,
fido2ClientService,
themeStateService,
);
portKeyForTabSpy = overlayBackground["portKeyForTab"];
@ -699,28 +708,28 @@ describe("OverlayBackground", () => {
describe("updating the overlay ciphers", () => {
const url = "https://jest-testing-website.com";
const tab = createChromeTabMock({ url });
const cipher1 = mock<CipherView>({
const loginCipher1 = mock<CipherView>({
id: "id-1",
localData: { lastUsedDate: 222 },
name: "name-1",
type: CipherType.Login,
login: { username: "username-1", uri: url },
});
const cipher2 = mock<CipherView>({
const cardCipher = mock<CipherView>({
id: "id-2",
localData: { lastUsedDate: 222 },
name: "name-2",
type: CipherType.Card,
card: { subTitle: "subtitle-2" },
});
const cipher3 = mock<CipherView>({
const loginCipher2 = mock<CipherView>({
id: "id-3",
localData: { lastUsedDate: 222 },
name: "name-3",
type: CipherType.Login,
login: { username: "username-3", uri: url },
});
const cipher4 = mock<CipherView>({
const identityCipher = mock<CipherView>({
id: "id-4",
localData: { lastUsedDate: 222 },
name: "name-4",
@ -732,6 +741,23 @@ describe("OverlayBackground", () => {
email: "email@example.com",
},
});
const passkeyCipher = mock<CipherView>({
id: "id-5",
localData: { lastUsedDate: 222 },
name: "name-5",
type: CipherType.Login,
login: {
username: "username-5",
uri: url,
fido2Credentials: [
mock<Fido2CredentialView>({
credentialId: "credential-id",
rpName: "credential-name",
userName: "credential-username",
}),
],
},
});
beforeEach(async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
@ -764,7 +790,7 @@ describe("OverlayBackground", () => {
it("closes the inline menu on the focused field's tab if current tab is different", async () => {
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
const previousTab = mock<chrome.tabs.Tab>({ id: 15 });
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: 15 });
@ -781,7 +807,7 @@ describe("OverlayBackground", () => {
it("queries all cipher types, sorts them by last used, and formats them for usage in the overlay", async () => {
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
await overlayBackground.updateOverlayCiphers();
@ -794,8 +820,8 @@ describe("OverlayBackground", () => {
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
new Map([
["inline-menu-cipher-0", cipher2],
["inline-menu-cipher-1", cipher1],
["inline-menu-cipher-0", cardCipher],
["inline-menu-cipher-1", loginCipher1],
]),
);
});
@ -803,7 +829,7 @@ describe("OverlayBackground", () => {
it("queries only login ciphers when not updating all cipher types", async () => {
overlayBackground["cardAndIdentityCiphers"] = new Set([]);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher3, cipher1]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher2, loginCipher1]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
await overlayBackground.updateOverlayCiphers(false);
@ -813,15 +839,15 @@ describe("OverlayBackground", () => {
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
new Map([
["inline-menu-cipher-0", cipher1],
["inline-menu-cipher-1", cipher3],
["inline-menu-cipher-0", loginCipher1],
["inline-menu-cipher-1", loginCipher2],
]),
);
});
it("queries all cipher types when the card and identity ciphers set is not built when only updating login ciphers", async () => {
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
await overlayBackground.updateOverlayCiphers(false);
@ -834,15 +860,15 @@ describe("OverlayBackground", () => {
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
new Map([
["inline-menu-cipher-0", cipher2],
["inline-menu-cipher-1", cipher1],
["inline-menu-cipher-0", cardCipher],
["inline-menu-cipher-1", loginCipher1],
]),
);
});
it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => {
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id });
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
@ -851,10 +877,11 @@ describe("OverlayBackground", () => {
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: false,
showPasskeysLabels: false,
ciphers: [
{
accountCreationFieldType: undefined,
favorite: cipher1.favorite,
favorite: loginCipher1.favorite,
icon: {
fallbackImage: "images/bwi-globe.png",
icon: "bwi-globe",
@ -864,9 +891,10 @@ describe("OverlayBackground", () => {
id: "inline-menu-cipher-0",
login: {
username: "username-1",
passkey: null,
},
name: "name-1",
reprompt: cipher1.reprompt,
reprompt: loginCipher1.reprompt,
type: CipherType.Login,
},
],
@ -878,7 +906,7 @@ describe("OverlayBackground", () => {
tabId: tab.id,
filledByCipherType: CipherType.Card,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
@ -887,10 +915,11 @@ describe("OverlayBackground", () => {
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: false,
showPasskeysLabels: false,
ciphers: [
{
accountCreationFieldType: undefined,
favorite: cipher2.favorite,
favorite: cardCipher.favorite,
icon: {
fallbackImage: "",
icon: "bwi-credit-card",
@ -898,9 +927,9 @@ describe("OverlayBackground", () => {
imageEnabled: true,
},
id: "inline-menu-cipher-0",
card: cipher2.card.subTitle,
name: cipher2.name,
reprompt: cipher2.reprompt,
card: cardCipher.card.subTitle,
name: cardCipher.name,
reprompt: cardCipher.reprompt,
type: CipherType.Card,
},
],
@ -914,7 +943,7 @@ describe("OverlayBackground", () => {
accountCreationFieldType: "text",
showInlineMenuAccountCreation: true,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher4, cipher2]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([identityCipher, cardCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
@ -923,10 +952,11 @@ describe("OverlayBackground", () => {
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: true,
showPasskeysLabels: false,
ciphers: [
{
accountCreationFieldType: "text",
favorite: cipher4.favorite,
favorite: identityCipher.favorite,
icon: {
fallbackImage: "",
icon: "bwi-id-card",
@ -934,12 +964,12 @@ describe("OverlayBackground", () => {
imageEnabled: true,
},
id: "inline-menu-cipher-1",
name: cipher4.name,
reprompt: cipher4.reprompt,
name: identityCipher.name,
reprompt: identityCipher.reprompt,
type: CipherType.Identity,
identity: {
fullName: `${cipher4.identity.firstName} ${cipher4.identity.lastName}`,
username: cipher4.identity.username,
fullName: `${identityCipher.identity.firstName} ${identityCipher.identity.lastName}`,
username: identityCipher.identity.username,
},
},
],
@ -952,7 +982,7 @@ describe("OverlayBackground", () => {
accountCreationFieldType: "text",
showInlineMenuAccountCreation: true,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher4]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, identityCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
@ -961,10 +991,11 @@ describe("OverlayBackground", () => {
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: true,
showPasskeysLabels: false,
ciphers: [
{
accountCreationFieldType: "text",
favorite: cipher4.favorite,
favorite: identityCipher.favorite,
icon: {
fallbackImage: "",
icon: "bwi-id-card",
@ -972,17 +1003,17 @@ describe("OverlayBackground", () => {
imageEnabled: true,
},
id: "inline-menu-cipher-0",
name: cipher4.name,
reprompt: cipher4.reprompt,
name: identityCipher.name,
reprompt: identityCipher.reprompt,
type: CipherType.Identity,
identity: {
fullName: `${cipher4.identity.firstName} ${cipher4.identity.lastName}`,
username: cipher4.identity.username,
fullName: `${identityCipher.identity.firstName} ${identityCipher.identity.lastName}`,
username: identityCipher.identity.username,
},
},
{
accountCreationFieldType: "text",
favorite: cipher1.favorite,
favorite: loginCipher1.favorite,
icon: {
fallbackImage: "images/bwi-globe.png",
icon: "bwi-globe",
@ -991,10 +1022,11 @@ describe("OverlayBackground", () => {
},
id: "inline-menu-cipher-1",
login: {
username: cipher1.login.username,
username: loginCipher1.login.username,
passkey: null,
},
name: cipher1.name,
reprompt: cipher1.reprompt,
name: loginCipher1.name,
reprompt: loginCipher1.reprompt,
type: CipherType.Login,
},
],
@ -1018,7 +1050,7 @@ describe("OverlayBackground", () => {
},
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([
cipher4,
identityCipher,
identityCipherWithoutUsername,
]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
@ -1029,10 +1061,11 @@ describe("OverlayBackground", () => {
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: true,
showPasskeysLabels: false,
ciphers: [
{
accountCreationFieldType: "email",
favorite: cipher4.favorite,
favorite: identityCipher.favorite,
icon: {
fallbackImage: "",
icon: "bwi-id-card",
@ -1040,12 +1073,12 @@ describe("OverlayBackground", () => {
imageEnabled: true,
},
id: "inline-menu-cipher-1",
name: cipher4.name,
reprompt: cipher4.reprompt,
name: identityCipher.name,
reprompt: identityCipher.reprompt,
type: CipherType.Identity,
identity: {
fullName: `${cipher4.identity.firstName} ${cipher4.identity.lastName}`,
username: cipher4.identity.email,
fullName: `${identityCipher.identity.firstName} ${identityCipher.identity.lastName}`,
username: identityCipher.identity.email,
},
},
],
@ -1058,7 +1091,7 @@ describe("OverlayBackground", () => {
accountCreationFieldType: "password",
showInlineMenuAccountCreation: true,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher4]);
cipherService.getAllDecryptedForUrl.mockResolvedValue([identityCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
@ -1067,10 +1100,89 @@ describe("OverlayBackground", () => {
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
showInlineMenuAccountCreation: true,
showPasskeysLabels: false,
ciphers: [],
});
});
});
it("adds available passkey ciphers to the inline menu", async () => {
availableAutofillCredentialsMock$.next(passkeyCipher.login.fido2Credentials);
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
tabId: tab.id,
filledByCipherType: CipherType.Login,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
await overlayBackground.updateOverlayCiphers();
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
ciphers: [
{
id: "inline-menu-cipher-0",
name: passkeyCipher.name,
type: CipherType.Login,
reprompt: passkeyCipher.reprompt,
favorite: passkeyCipher.favorite,
icon: {
fallbackImage: "images/bwi-globe.png",
icon: "bwi-globe",
image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
imageEnabled: true,
},
accountCreationFieldType: undefined,
login: {
username: passkeyCipher.login.username,
passkey: {
rpName: passkeyCipher.login.fido2Credentials[0].rpName,
userName: passkeyCipher.login.fido2Credentials[0].userName,
},
},
},
{
id: "inline-menu-cipher-0",
name: passkeyCipher.name,
type: CipherType.Login,
reprompt: passkeyCipher.reprompt,
favorite: passkeyCipher.favorite,
icon: {
fallbackImage: "images/bwi-globe.png",
icon: "bwi-globe",
image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
imageEnabled: true,
},
accountCreationFieldType: undefined,
login: {
username: passkeyCipher.login.username,
passkey: null,
},
},
{
id: "inline-menu-cipher-1",
name: loginCipher1.name,
type: CipherType.Login,
reprompt: loginCipher1.reprompt,
favorite: loginCipher1.favorite,
icon: {
fallbackImage: "images/bwi-globe.png",
icon: "bwi-globe",
image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
imageEnabled: true,
},
accountCreationFieldType: undefined,
login: {
username: loginCipher1.login.username,
passkey: null,
},
},
],
showInlineMenuAccountCreation: false,
showPasskeysLabels: true,
});
});
});
describe("extension message handlers", () => {
@ -1562,6 +1674,7 @@ describe("OverlayBackground", () => {
command: "updateAutofillInlineMenuListCiphers",
ciphers: [],
showInlineMenuAccountCreation: true,
showPasskeysLabels: false,
});
});
@ -2660,6 +2773,41 @@ describe("OverlayBackground", () => {
expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code");
});
it("triggers passkey authentication through mediated conditional UI", async () => {
const fido2Credential = mock<Fido2CredentialView>({ credentialId: "credential-id" });
const cipher1 = mock<CipherView>({
id: "inline-menu-cipher-1",
login: {
username: "username1",
password: "password1",
fido2Credentials: [fido2Credential],
},
});
overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher1]]);
const pageDetailsForTab = {
frameId: sender.frameId,
tab: sender.tab,
details: pageDetails,
};
overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([
[sender.frameId, pageDetailsForTab],
]);
autofillService.isPasswordRepromptRequired.mockResolvedValue(false);
sendPortMessage(listMessageConnectorSpy, {
command: "fillAutofillInlineMenuCipher",
inlineMenuCipherId: "inline-menu-cipher-1",
usePasskey: true,
portKey,
});
await flushPromises();
expect(fido2ClientService.autofillCredential).toHaveBeenCalledWith(
sender.tab.id,
fido2Credential.credentialId,
);
});
});
describe("addNewVaultItem message handler", () => {

View File

@ -1,5 +1,12 @@
import { firstValueFrom, merge, Subject, throttleTime } from "rxjs";
import { debounceTime, switchMap } from "rxjs/operators";
import {
firstValueFrom,
merge,
ReplaySubject,
Subject,
throttleTime,
switchMap,
debounceTime,
} from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@ -11,6 +18,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { Fido2ClientService } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -21,6 +29,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
@ -41,6 +50,7 @@ import { generateRandomChars } from "../utils";
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
import {
BuildCipherDataParams,
CloseInlineMenuMessage,
CurrentAddNewItemData,
FocusedFieldData,
@ -66,6 +76,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private readonly openUnlockPopout = openUnlockPopout;
private readonly openViewVaultItemPopout = openViewVaultItemPopout;
private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout;
private readonly storeInlineMenuFido2CredentialsSubject = new ReplaySubject<number>(1);
private pageDetailsForTab: PageDetailsForTab = {};
private subFrameOffsetsForTab: SubFrameOffsetsForTab = {};
private portKeyForTab: Record<number, string> = {};
@ -73,6 +84,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private inlineMenuButtonPort: chrome.runtime.Port;
private inlineMenuListPort: chrome.runtime.Port;
private inlineMenuCiphers: Map<string, CipherView> = new Map();
private inlineMenuFido2Credentials: Set<string> = new Set();
private inlineMenuPageTranslations: Record<string, string>;
private inlineMenuPosition: InlineMenuPosition = {};
private cardAndIdentityCiphers: Set<CipherView> | null = null;
@ -91,6 +103,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private isFieldCurrentlyFilling: boolean = false;
private isInlineMenuButtonVisible: boolean = false;
private isInlineMenuListVisible: boolean = false;
private showPasskeysLabelsWithinInlineMenu: boolean = false;
private iconsServerUrl: string;
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
autofillOverlayElementClosed: ({ message, sender }) =>
@ -159,6 +172,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private fido2ClientService: Fido2ClientService,
private themeStateService: ThemeStateService,
) {
this.initOverlayEventObservables();
@ -178,6 +192,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* Initializes event observables that handle events which affect the overlay's behavior.
*/
private initOverlayEventObservables() {
this.storeInlineMenuFido2CredentialsSubject
.pipe(switchMap((tabId) => this.fido2ClientService.availableAutofillCredentials$(tabId)))
.subscribe((credentials) => this.storeInlineMenuFido2Credentials(credentials));
this.repositionInlineMenuSubject
.pipe(
debounceTime(1000),
@ -252,6 +269,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
}
if (!currentTab) {
return;
}
this.inlineMenuFido2Credentials.clear();
this.storeInlineMenuFido2CredentialsSubject.next(currentTab.id);
this.inlineMenuCiphers = new Map();
const ciphersViews = await this.getCipherViews(currentTab, updateAllCipherTypes);
for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) {
@ -263,6 +287,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
command: "updateAutofillInlineMenuListCiphers",
ciphers,
showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(),
showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu,
});
}
@ -280,9 +305,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return this.getAllCipherTypeViews(currentTab);
}
const cipherViews = (
await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "")
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
const cipherViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url || "")).sort(
(a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b),
);
return this.cardAndIdentityCiphers
? cipherViews.concat(...this.cardAndIdentityCiphers)
@ -301,7 +326,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.cardAndIdentityCiphers.clear();
const cipherViews = (
await this.cipherService.getAllDecryptedForUrl(currentTab.url, [
await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", [
CipherType.Card,
CipherType.Identity,
])
@ -331,6 +356,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers);
let inlineMenuCipherData: InlineMenuCipherData[];
this.showPasskeysLabelsWithinInlineMenu = false;
if (this.showInlineMenuAccountCreation()) {
inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers(
@ -363,7 +389,12 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (cipher.type === CipherType.Login) {
accountCreationLoginCiphers.push(
this.buildCipherData(inlineMenuCipherId, cipher, showFavicons, true),
this.buildCipherData({
inlineMenuCipherId,
cipher,
showFavicons,
showInlineMenuAccountCreation: true,
}),
);
continue;
}
@ -378,7 +409,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
inlineMenuCipherData.push(
this.buildCipherData(inlineMenuCipherId, cipher, showFavicons, true, identity),
this.buildCipherData({
inlineMenuCipherId,
cipher,
showFavicons,
showInlineMenuAccountCreation: true,
identityData: identity,
}),
);
}
@ -400,6 +437,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
showFavicons: boolean,
) {
const inlineMenuCipherData: InlineMenuCipherData[] = [];
const passkeyCipherData: InlineMenuCipherData[] = [];
for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) {
const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex];
@ -407,12 +445,43 @@ export class OverlayBackground implements OverlayBackgroundInterface {
continue;
}
inlineMenuCipherData.push(this.buildCipherData(inlineMenuCipherId, cipher, showFavicons));
if (this.showCipherAsPasskey(cipher)) {
passkeyCipherData.push(
this.buildCipherData({
inlineMenuCipherId,
cipher,
showFavicons,
hasPasskey: true,
}),
);
}
inlineMenuCipherData.push(this.buildCipherData({ inlineMenuCipherId, cipher, showFavicons }));
}
if (passkeyCipherData.length) {
this.showPasskeysLabelsWithinInlineMenu =
passkeyCipherData.length > 0 && inlineMenuCipherData.length > 0;
return passkeyCipherData.concat(inlineMenuCipherData);
}
return inlineMenuCipherData;
}
/**
* Identifies whether we should show the cipher as a passkey in the inline menu list.
*
* @param cipher - The cipher to check
*/
private showCipherAsPasskey(cipher: CipherView): boolean {
return (
cipher.type === CipherType.Login &&
cipher.login.fido2Credentials?.length > 0 &&
(this.inlineMenuFido2Credentials.size === 0 ||
this.inlineMenuFido2Credentials.has(cipher.login.fido2Credentials[0].credentialId))
);
}
/**
* Builds the cipher data for the inline menu list.
*
@ -420,15 +489,17 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param cipher - The cipher to build data for
* @param showFavicons - Identifies whether favicons should be shown
* @param showInlineMenuAccountCreation - Identifies whether the inline menu is for account creation
* @param hasPasskey - Identifies whether the cipher has a FIDO2 credential
* @param identityData - Pre-created identity data
*/
private buildCipherData(
inlineMenuCipherId: string,
cipher: CipherView,
showFavicons: boolean,
showInlineMenuAccountCreation: boolean = false,
identityData?: { fullName: string; username?: string },
): InlineMenuCipherData {
private buildCipherData({
inlineMenuCipherId,
cipher,
showFavicons,
showInlineMenuAccountCreation,
hasPasskey,
identityData,
}: BuildCipherDataParams): InlineMenuCipherData {
const inlineMenuData: InlineMenuCipherData = {
id: inlineMenuCipherId,
name: cipher.name,
@ -440,7 +511,15 @@ export class OverlayBackground implements OverlayBackgroundInterface {
};
if (cipher.type === CipherType.Login) {
inlineMenuData.login = { username: cipher.login.username };
inlineMenuData.login = {
username: cipher.login.username,
passkey: hasPasskey
? {
rpName: cipher.login.fido2Credentials[0].rpName,
userName: cipher.login.fido2Credentials[0].userName,
}
: null,
};
return inlineMenuData;
}
@ -512,6 +591,17 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return this.inlineMenuCiphers.size === 0;
}
/**
* Stores the credential ids associated with a FIDO2 conditional mediated ui request.
*
* @param credentials - The FIDO2 credentials to store
*/
private storeInlineMenuFido2Credentials(credentials: Fido2CredentialView[]) {
credentials
.map((credential) => credential.credentialId)
.forEach((credentialId) => this.inlineMenuFido2Credentials.add(credentialId));
}
/**
* Gets the currently focused field and closes the inline menu on that tab.
*/
@ -749,10 +839,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* the selected cipher at the top of the list of ciphers.
*
* @param inlineMenuCipherId - Cipher ID corresponding to the inlineMenuCiphers map. Does not correspond to the actual cipher's ID.
* @param usePasskey - Identifies whether the cipher has a FIDO2 credential
* @param sender - The sender of the port message
*/
private async fillInlineMenuCipher(
{ inlineMenuCipherId }: OverlayPortMessage,
{ inlineMenuCipherId, usePasskey }: OverlayPortMessage,
{ sender }: chrome.runtime.Port,
) {
const pageDetails = this.pageDetailsForTab[sender.tab.id];
@ -762,6 +853,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId);
if (usePasskey && cipher.login?.hasFido2Credentials) {
await this.fido2ClientService.autofillCredential(
sender.tab.id,
cipher.login.fido2Credentials[0].credentialId,
);
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
return;
}
if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) {
return;
}
@ -777,6 +878,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.platformUtilsService.copyToClipboard(totpCode);
}
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
}
/**
* Sets the most recently used cipher at the top of the list of ciphers.
*
* @param inlineMenuCipherId - The ID of the inline menu cipher
* @param cipher - The cipher to set as the most recently used
*/
private updateLastUsedInlineMenuCipher(inlineMenuCipherId: string, cipher: CipherView) {
this.inlineMenuCiphers = new Map([[inlineMenuCipherId, cipher], ...this.inlineMenuCiphers]);
}
@ -1163,6 +1274,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
command: "updateAutofillInlineMenuListCiphers",
ciphers: await this.getInlineMenuCipherData(),
showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(),
showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu,
});
}
@ -1214,6 +1326,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private async openInlineMenu(isFocusingFieldElement = false, isOpeningFullInlineMenu = false) {
this.clearDelayedInlineMenuClosure();
const currentTab = await BrowserApi.getTabFromCurrentWindowId();
if (!currentTab) {
return;
}
await BrowserApi.tabSendMessage(
currentTab,
@ -1224,8 +1339,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
authStatus: await this.getAuthStatus(),
},
{
frameId:
this.focusedFieldData?.tabId === currentTab?.id ? this.focusedFieldData.frameId : 0,
frameId: this.focusedFieldData?.tabId === currentTab.id ? this.focusedFieldData.frameId : 0,
},
);
}
@ -1367,6 +1481,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
newIdentity: this.i18nService.translate("newIdentity"),
addNewIdentityItem: this.i18nService.translate("addNewIdentityItemAria"),
cardNumberEndsWith: this.i18nService.translate("cardNumberEndsWith"),
passkeys: this.i18nService.translate("passkeys"),
passwords: this.i18nService.translate("passwords"),
logInWithPasskey: this.i18nService.translate("logInWithPasskeyAriaLabel"),
};
}
@ -2064,6 +2181,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
: AutofillOverlayPort.ButtonMessageConnector,
filledByCipherType: this.focusedFieldData?.filledByCipherType,
showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(),
showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu,
});
this.updateInlineMenuPosition(
{

View File

@ -1,5 +1,3 @@
import { FallbackRequestedError } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { WebauthnUtils } from "../../../vault/fido2/webauthn-utils";
import { MessageType } from "./messaging/message";
@ -126,13 +124,47 @@ import { Messenger } from "./messaging/messenger";
return await browserCredentials.get(options);
}
const abortSignal = options?.signal || new AbortController().signal;
const fallbackSupported = browserNativeWebauthnSupport;
try {
if (options?.mediation && options.mediation !== "optional") {
throw new FallbackRequestedError();
}
if (options?.mediation && options.mediation === "conditional") {
const internalAbortControllers = [new AbortController(), new AbortController()];
const bitwardenResponse = async (internalAbortController: AbortController) => {
try {
const response = await messenger.request(
{
type: MessageType.CredentialGetRequest,
data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported),
},
internalAbortController.signal,
);
if (response.type !== MessageType.CredentialGetResponse) {
throw new Error("Something went wrong.");
}
return WebauthnUtils.mapCredentialAssertResult(response.result);
} catch {
// Ignoring error
}
};
const browserResponse = (internalAbortController: AbortController) =>
browserCredentials.get({ ...options, signal: internalAbortController.signal });
const abortListener = () => {
internalAbortControllers.forEach((controller) => controller.abort());
};
abortSignal.addEventListener("abort", abortListener);
const response = await Promise.race([
bitwardenResponse(internalAbortControllers[0]),
browserResponse(internalAbortControllers[1]),
]);
abortSignal.removeEventListener("abort", abortListener);
internalAbortControllers.forEach((controller) => controller.abort());
return response;
}
try {
const response = await messenger.request(
{
type: MessageType.CredentialGetRequest,

View File

@ -128,6 +128,17 @@ describe("Fido2 page script with native WebAuthn support", () => {
mockCredentialAssertResult,
);
});
it("initiates a conditional mediated webauth request", async () => {
mockCredentialRequestOptions.mediation = "conditional";
mockCredentialRequestOptions.signal = new AbortController().signal;
await navigator.credentials.get(mockCredentialRequestOptions);
expect(WebauthnUtils.mapCredentialAssertResult).toHaveBeenCalledWith(
mockCredentialAssertResult,
);
});
});
describe("destroy", () => {

View File

@ -18,6 +18,7 @@ export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage &
ciphers?: InlineMenuCipherData[];
filledByCipherType?: CipherType;
showInlineMenuAccountCreation?: boolean;
showPasskeysLabels?: boolean;
portKey: string;
};

View File

@ -478,7 +478,6 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username: "
aria-label="fillCredentialsFor "
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
@ -910,7 +909,6 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username: "
aria-label="fillCredentialsFor "
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
@ -1342,7 +1340,6 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container"
>
<button
aria-description="username: "
aria-label="fillCredentialsFor "
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
@ -1619,6 +1616,507 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
</div>
`;
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creating a list of passkeys renders the passkeys list item views 1`] = `
<div
class="inline-menu-list-container theme_light"
>
<ul
class="inline-menu-list-actions"
role="list"
>
<li
class="inline-menu-list-heading"
/>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username1"
aria-label=" https://example.com"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="https://example.com"
>
https://example.com
</span>
<span
class="cipher-subtitle cipher-subtitle--passkey"
title="username1"
>
<svg
aria-hidden="true"
fill="none"
height="15"
viewBox="0 0 14 15"
width="14"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.35 12.112a.713.713 0 1 1 0-1.426.713.713 0 0 1 0 1.426Z"
fill="#6D757E"
/>
<path
clip-rule="evenodd"
d="M4.597 7.695a3.5 3.5 0 1 1 3.741 0A5.33 5.33 0 0 1 10.5 9.186c.154.172.29.328.384.461l1.562-.001L14 11.14l-2.188 1.952-.874-.875-.876.875-.874-.875-.876.84-2.613-.003a3.152 3.152 0 0 1-2.634 1.307c-1.729-.036-3.101-1.436-3.064-3.127C.038 9.543 1.469 8.2 3.199 8.237c.098.002.195.009.291.02a6.76 6.76 0 0 1 .296-.181c.257-.149.528-.276.81-.381Zm1.176 1.957 3.952-.004a4.11 4.11 0 0 0-.498-.462 4.452 4.452 0 0 0-2.76-.95c-.647 0-1.262.137-1.817.384a3.12 3.12 0 0 1 1.123 1.032Zm-1.93-4.916a2.625 2.625 0 1 0 5.25 0 2.625 2.625 0 0 0-5.25 0Zm1.407 7.442-.262.366a2.277 2.277 0 0 1-1.904.942C1.819 13.459.85 12.442.876 11.253c.025-1.19 1.04-2.168 2.304-2.141a2.27 2.27 0 0 1 1.86 1.019l.26.396 6.794-.006.619.595-.866.773-.91-.909-.874.875-.863-.862-1.239 1.19-2.711-.005Z"
fill="#6D757E"
fill-rule="evenodd"
/>
</svg>
username1
</span>
</span>
</button>
<button
aria-label="view https://example.com, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username2"
aria-label=" https://example.com"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="https://example.com"
>
https://example.com
</span>
<span
class="cipher-subtitle cipher-subtitle--passkey"
title="username2"
>
<svg
aria-hidden="true"
fill="none"
height="15"
viewBox="0 0 14 15"
width="14"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.35 12.112a.713.713 0 1 1 0-1.426.713.713 0 0 1 0 1.426Z"
fill="#6D757E"
/>
<path
clip-rule="evenodd"
d="M4.597 7.695a3.5 3.5 0 1 1 3.741 0A5.33 5.33 0 0 1 10.5 9.186c.154.172.29.328.384.461l1.562-.001L14 11.14l-2.188 1.952-.874-.875-.876.875-.874-.875-.876.84-2.613-.003a3.152 3.152 0 0 1-2.634 1.307c-1.729-.036-3.101-1.436-3.064-3.127C.038 9.543 1.469 8.2 3.199 8.237c.098.002.195.009.291.02a6.76 6.76 0 0 1 .296-.181c.257-.149.528-.276.81-.381Zm1.176 1.957 3.952-.004a4.11 4.11 0 0 0-.498-.462 4.452 4.452 0 0 0-2.76-.95c-.647 0-1.262.137-1.817.384a3.12 3.12 0 0 1 1.123 1.032Zm-1.93-4.916a2.625 2.625 0 1 0 5.25 0 2.625 2.625 0 0 0-5.25 0Zm1.407 7.442-.262.366a2.277 2.277 0 0 1-1.904.942C1.819 13.459.85 12.442.876 11.253c.025-1.19 1.04-2.168 2.304-2.141a2.27 2.27 0 0 1 1.86 1.019l.26.396 6.794-.006.619.595-.866.773-.91-.909-.874.875-.863-.862-1.239 1.19-2.711-.005Z"
fill="#6D757E"
fill-rule="evenodd"
/>
</svg>
username2
</span>
</span>
</button>
<button
aria-label="view https://example.com, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username3"
aria-label=" website login 3"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 3"
>
website login 3
</span>
<span
class="cipher-subtitle cipher-subtitle--passkey"
title="https://example.com"
>
<svg
aria-hidden="true"
fill="none"
height="15"
viewBox="0 0 14 15"
width="14"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.35 12.112a.713.713 0 1 1 0-1.426.713.713 0 0 1 0 1.426Z"
fill="#6D757E"
/>
<path
clip-rule="evenodd"
d="M4.597 7.695a3.5 3.5 0 1 1 3.741 0A5.33 5.33 0 0 1 10.5 9.186c.154.172.29.328.384.461l1.562-.001L14 11.14l-2.188 1.952-.874-.875-.876.875-.874-.875-.876.84-2.613-.003a3.152 3.152 0 0 1-2.634 1.307c-1.729-.036-3.101-1.436-3.064-3.127C.038 9.543 1.469 8.2 3.199 8.237c.098.002.195.009.291.02a6.76 6.76 0 0 1 .296-.181c.257-.149.528-.276.81-.381Zm1.176 1.957 3.952-.004a4.11 4.11 0 0 0-.498-.462 4.452 4.452 0 0 0-2.76-.95c-.647 0-1.262.137-1.817.384a3.12 3.12 0 0 1 1.123 1.032Zm-1.93-4.916a2.625 2.625 0 1 0 5.25 0 2.625 2.625 0 0 0-5.25 0Zm1.407 7.442-.262.366a2.277 2.277 0 0 1-1.904.942C1.819 13.459.85 12.442.876 11.253c.025-1.19 1.04-2.168 2.304-2.141a2.27 2.27 0 0 1 1.86 1.019l.26.396 6.794-.006.619.595-.866.773-.91-.909-.874.875-.863-.862-1.239 1.19-2.711-.005Z"
fill="#6D757E"
fill-rule="evenodd"
/>
</svg>
https://example.com
</span>
<span
class="cipher-subtitle"
title="username3"
>
username3
</span>
</span>
</button>
<button
aria-label="view website login 3, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-heading"
/>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username1"
aria-label="fillCredentialsFor website login 1"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 1"
>
website login 1
</span>
<span
class="cipher-subtitle"
title="username1"
>
username1
</span>
</span>
</button>
<button
aria-label="view website login 1, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username2"
aria-label="fillCredentialsFor website login 2"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 2"
>
website login 2
</span>
<span
class="cipher-subtitle"
title="username2"
>
username2
</span>
</span>
</button>
<button
aria-label="view website login 2, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username3"
aria-label="fillCredentialsFor website login 3"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 3"
>
website login 3
</span>
<span
class="cipher-subtitle"
title="username3"
>
username3
</span>
</span>
</button>
<button
aria-label="view website login 3, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
</ul>
</div>
`;
exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline menu for an unauthenticated user creates the views for the locked inline menu 1`] = `
<div
class="inline-menu-list-container theme_light"

View File

@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { CipherType } from "@bitwarden/common/vault/enums";
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
import {
createAutofillOverlayCipherDataMock,
createInitAutofillInlineMenuListMessageMock,
@ -236,7 +237,12 @@ describe("AutofillInlineMenuList", () => {
fillCipherButton.dispatchEvent(new Event("click"));
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
{ command: "fillAutofillInlineMenuCipher", inlineMenuCipherId: "1", portKey },
{
command: "fillAutofillInlineMenuCipher",
inlineMenuCipherId: "1",
usePasskey: false,
portKey,
},
"*",
);
});
@ -504,6 +510,178 @@ describe("AutofillInlineMenuList", () => {
});
});
});
describe("creating a list of passkeys", () => {
let passkeyCipher1: InlineMenuCipherData;
let passkeyCipher2: InlineMenuCipherData;
let passkeyCipher3: InlineMenuCipherData;
let loginCipher1: InlineMenuCipherData;
let loginCipher2: InlineMenuCipherData;
let loginCipher3: InlineMenuCipherData;
let loginCipher4: InlineMenuCipherData;
const borderClass = "inline-menu-list-heading--bordered";
beforeEach(() => {
passkeyCipher1 = createAutofillOverlayCipherDataMock(1, {
name: "https://example.com",
login: {
username: "username1",
passkey: {
rpName: "https://example.com",
userName: "username1",
},
},
});
passkeyCipher2 = createAutofillOverlayCipherDataMock(2, {
name: "https://example.com",
login: {
username: "",
passkey: {
rpName: "https://example.com",
userName: "username2",
},
},
});
passkeyCipher3 = createAutofillOverlayCipherDataMock(3, {
login: {
username: "username3",
passkey: {
rpName: "https://example.com",
userName: "username3",
},
},
});
loginCipher1 = createAutofillOverlayCipherDataMock(1, {
login: {
username: "username1",
passkey: null,
},
});
loginCipher2 = createAutofillOverlayCipherDataMock(2, {
login: {
username: "username2",
passkey: null,
},
});
loginCipher3 = createAutofillOverlayCipherDataMock(3, {
login: {
username: "username3",
passkey: null,
},
});
loginCipher4 = createAutofillOverlayCipherDataMock(4, {
login: {
username: "username4",
passkey: null,
},
});
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({
ciphers: [
passkeyCipher1,
passkeyCipher2,
passkeyCipher3,
loginCipher1,
loginCipher2,
loginCipher3,
loginCipher4,
],
showPasskeysLabels: true,
portKey,
}),
);
});
it("renders the passkeys list item views", () => {
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
});
describe("passkeys headings on scroll", () => {
it("adds a border class to the passkeys and login headings when the user scrolls the cipher list container", () => {
autofillInlineMenuList["ciphersList"].scrollTop = 300;
autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll"));
expect(
autofillInlineMenuList["passkeysHeadingElement"].classList.contains(borderClass),
).toBe(true);
expect(autofillInlineMenuList["passkeysHeadingElement"].style.position).toBe(
"relative",
);
expect(
autofillInlineMenuList["loginHeadingElement"].classList.contains(borderClass),
).toBe(true);
});
it("removes the border class from the passkeys and login headings when the user scrolls the cipher list container to the top", () => {
jest.useFakeTimers();
autofillInlineMenuList["ciphersList"].scrollTop = 300;
autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll"));
jest.advanceTimersByTime(75);
autofillInlineMenuList["ciphersList"].scrollTop = -1;
autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll"));
expect(
autofillInlineMenuList["passkeysHeadingElement"].classList.contains(borderClass),
).toBe(false);
expect(autofillInlineMenuList["passkeysHeadingElement"].style.position).toBe("");
expect(
autofillInlineMenuList["loginHeadingElement"].classList.contains(borderClass),
).toBe(false);
});
it("loads each page of ciphers until the list of updated ciphers is exhausted", () => {
jest.useFakeTimers();
autofillInlineMenuList["ciphersList"].scrollTop = 10;
jest.spyOn(autofillInlineMenuList as any, "loadPageOfCiphers");
autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll"));
jest.advanceTimersByTime(1000);
autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll"));
jest.runAllTimers();
expect(autofillInlineMenuList["loadPageOfCiphers"]).toHaveBeenCalledTimes(1);
});
});
it("skips the logins heading when the user presses ArrowDown to focus the next list item", () => {
const cipherContainerElements =
autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll("li");
const viewCipherButton = cipherContainerElements[3].querySelector(".view-cipher-button");
const fillCipherButton = cipherContainerElements[5].querySelector(".fill-cipher-button");
jest.spyOn(fillCipherButton as HTMLElement, "focus");
viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
expect((fillCipherButton as HTMLElement).focus).toBeCalled();
});
it("skips the passkeys heading when the user presses ArrowDown to focus the first list item", () => {
const cipherContainerElements =
autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll("li");
const viewCipherButton = cipherContainerElements[7].querySelector(".view-cipher-button");
const fillCipherButton = cipherContainerElements[1].querySelector(".fill-cipher-button");
jest.spyOn(fillCipherButton as HTMLElement, "focus");
viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" }));
expect((fillCipherButton as HTMLElement).focus).toBeCalled();
});
it("skips the logins heading when the user presses ArrowUp to focus the previous list item", () => {
const cipherContainerElements =
autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll("li");
const viewCipherButton = cipherContainerElements[5].querySelector(".view-cipher-button");
const fillCipherButton = cipherContainerElements[3].querySelector(".fill-cipher-button");
jest.spyOn(fillCipherButton as HTMLElement, "focus");
viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowUp" }));
expect((fillCipherButton as HTMLElement).focus).toBeCalled();
});
});
});
});

View File

@ -1,12 +1,18 @@
import "@webcomponents/custom-elements";
import "lit/polyfill-support.js";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EVENTS } from "@bitwarden/common/autofill/constants";
import { EVENTS, UPDATE_PASSKEYS_HEADINGS_ON_SCROLL } from "@bitwarden/common/autofill/constants";
import { CipherType } from "@bitwarden/common/vault/enums";
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
import { buildSvgDomElement } from "../../../../utils";
import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../../utils/svg-icons";
import { buildSvgDomElement, throttle } from "../../../../utils";
import {
globeIcon,
lockIcon,
plusIcon,
viewCipherIcon,
passkeyIcon,
} from "../../../../utils/svg-icons";
import {
AutofillInlineMenuListWindowMessageHandlers,
InitAutofillInlineMenuListMessage,
@ -24,8 +30,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
private currentCipherIndex = 0;
private filledByCipherType: CipherType;
private showInlineMenuAccountCreation: boolean;
private readonly showCiphersPerPage = 6;
private showPasskeysLabels: boolean;
private newItemButtonElement: HTMLButtonElement;
private passkeysHeadingElement: HTMLLIElement;
private loginHeadingElement: HTMLLIElement;
private lastPasskeysListItem: HTMLLIElement;
private passkeysHeadingHeight: number;
private lastPasskeysListItemHeight: number;
private ciphersListHeight: number;
private readonly showCiphersPerPage = 6;
private readonly headingBorderClass = "inline-menu-list-heading--bordered";
private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers =
{
initAutofillInlineMenuList: ({ message }) => this.initAutofillInlineMenuList(message),
@ -53,6 +67,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param portKey - Background generated key that allows the port to communicate with the background.
* @param filledByCipherType - The type of cipher that fills the current field.
* @param showInlineMenuAccountCreation - Whether identity ciphers are shown on login fields.
* @param showPasskeysLabels - Whether passkeys labels are shown in the inline menu list.
*/
private async initAutofillInlineMenuList({
translations,
@ -63,6 +78,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
portKey,
filledByCipherType,
showInlineMenuAccountCreation,
showPasskeysLabels,
}: InitAutofillInlineMenuListMessage) {
const linkElement = await this.initAutofillInlineMenuPage(
"list",
@ -72,6 +88,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
);
this.filledByCipherType = filledByCipherType;
this.showPasskeysLabels = showPasskeysLabels;
const themeClass = `theme_${theme}`;
globalThis.document.documentElement.classList.add(themeClass);
@ -155,9 +172,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
this.ciphersList = globalThis.document.createElement("ul");
this.ciphersList.classList.add("inline-menu-list-actions");
this.ciphersList.setAttribute("role", "list");
this.ciphersList.addEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent, {
passive: true,
});
this.setupCipherListScrollListeners();
this.loadPageOfCiphers();
@ -288,8 +303,35 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
this.currentCipherIndex++;
}
if (this.currentCipherIndex >= this.ciphers.length) {
this.ciphersList.removeEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent);
if (!this.showPasskeysLabels && this.allCiphersLoaded()) {
this.ciphersList.removeEventListener(EVENTS.SCROLL, this.updateCiphersListOnScroll);
}
}
/**
* Validates whether the list of ciphers has been fully loaded.
*/
private allCiphersLoaded() {
return this.currentCipherIndex >= this.ciphers.length;
}
/**
* Sets up the scroll listeners for the ciphers list. These are used to trigger an update of
* the list of ciphers when the user scrolls to the bottom of the list. Also sets up the
* scroll listeners that reposition the passkeys and login headings when the user scrolls.
*/
private setupCipherListScrollListeners() {
const options = { passive: true };
this.ciphersList.addEventListener(EVENTS.SCROLL, this.updateCiphersListOnScroll, options);
if (this.showPasskeysLabels) {
this.ciphersList.addEventListener(
EVENTS.SCROLL,
this.useEventHandlersMemo(
throttle(() => this.updatePasskeysHeadingsOnScroll(this.ciphersList.scrollTop), 50),
UPDATE_PASSKEYS_HEADINGS_ON_SCROLL,
),
options,
);
}
}
@ -297,7 +339,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* Handles updating the list of ciphers when the
* user scrolls to the bottom of the list.
*/
private handleCiphersListScrollEvent = () => {
private updateCiphersListOnScroll = () => {
if (this.cipherListScrollIsDebounced) {
return;
}
@ -318,22 +360,109 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
*/
private handleDebouncedScrollEvent = () => {
this.cipherListScrollIsDebounced = false;
const cipherListScrollTop = this.ciphersList.scrollTop;
this.updatePasskeysHeadingsOnScroll(cipherListScrollTop);
if (this.allCiphersLoaded()) {
return;
}
if (!this.ciphersListHeight) {
this.ciphersListHeight = this.ciphersList.offsetHeight;
}
const scrollPercentage =
(this.ciphersList.scrollTop /
(this.ciphersList.scrollHeight - this.ciphersList.offsetHeight)) *
100;
(cipherListScrollTop / (this.ciphersList.scrollHeight - this.ciphersListHeight)) * 100;
if (scrollPercentage >= 80) {
this.loadPageOfCiphers();
}
};
/**
* Updates the passkeys and login headings when the user scrolls the ciphers list.
*
* @param cipherListScrollTop - The current scroll top position of the ciphers list.
*/
private updatePasskeysHeadingsOnScroll = (cipherListScrollTop: number) => {
if (!this.showPasskeysLabels) {
return;
}
if (this.passkeysHeadingElement) {
this.togglePasskeysHeadingAnchored(cipherListScrollTop);
this.togglePasskeysHeadingBorder(cipherListScrollTop);
}
if (this.loginHeadingElement) {
this.toggleLoginHeadingBorder(cipherListScrollTop);
}
};
/**
* Anchors the passkeys heading to the top of the last passkey item when the user scrolls.
*
* @param cipherListScrollTop - The current scroll top position of the ciphers list.
*/
private togglePasskeysHeadingAnchored(cipherListScrollTop: number) {
if (!this.passkeysHeadingHeight) {
this.passkeysHeadingHeight = this.passkeysHeadingElement.offsetHeight;
}
const passkeysHeadingOffset = this.lastPasskeysListItem.offsetTop - this.passkeysHeadingHeight;
if (cipherListScrollTop >= passkeysHeadingOffset) {
this.passkeysHeadingElement.style.position = "relative";
this.passkeysHeadingElement.style.top = `${passkeysHeadingOffset}px`;
return;
}
this.passkeysHeadingElement.setAttribute("style", "");
}
/**
* Toggles a border on the passkeys heading on scroll, adding it when the user has
* scrolled at all and removing it once the user scrolls back to the top.
*
* @param cipherListScrollTop - The current scroll top position of the ciphers list.
*/
private togglePasskeysHeadingBorder(cipherListScrollTop: number) {
if (cipherListScrollTop < 1) {
this.passkeysHeadingElement.classList.remove(this.headingBorderClass);
return;
}
this.passkeysHeadingElement.classList.add(this.headingBorderClass);
}
/**
* Toggles a border on the login heading on scroll, adding it when the user has
* scrolled past the last passkey item and removing it once the user scrolls back up.
*
* @param cipherListScrollTop - The current scroll top position of the ciphers list.
*/
private toggleLoginHeadingBorder(cipherListScrollTop: number) {
if (!this.lastPasskeysListItemHeight) {
this.lastPasskeysListItemHeight = this.lastPasskeysListItem.offsetHeight;
}
const lastPasskeyOffset = this.lastPasskeysListItem.offsetTop + this.lastPasskeysListItemHeight;
if (cipherListScrollTop < lastPasskeyOffset) {
this.loginHeadingElement.classList.remove(this.headingBorderClass);
return;
}
this.loginHeadingElement.classList.add(this.headingBorderClass);
}
/**
* Builds the list item for a given cipher.
*
* @param cipher - The cipher to build the list item for.
*/
private buildInlineMenuListActionsItem(cipher: InlineMenuCipherData) {
this.buildPasskeysHeadingElements(cipher);
const fillCipherElement = this.buildFillCipherElement(cipher);
const viewCipherElement = this.buildViewCipherElement(cipher);
@ -346,9 +475,43 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
inlineMenuListActionsItem.classList.add("inline-menu-list-actions-item");
inlineMenuListActionsItem.appendChild(cipherContainerElement);
if (this.showPasskeysLabels && cipher.login?.passkey) {
this.lastPasskeysListItem = inlineMenuListActionsItem;
}
return inlineMenuListActionsItem;
}
/**
* Builds the passkeys and login headings for the list of cipher items.
*
* @param cipher - The cipher that will follow the heading.
*/
private buildPasskeysHeadingElements(cipher: InlineMenuCipherData) {
if (!this.showPasskeysLabels || (this.passkeysHeadingElement && this.loginHeadingElement)) {
return;
}
const passkeyData = cipher.login?.passkey;
if (!this.passkeysHeadingElement && passkeyData) {
this.passkeysHeadingElement = globalThis.document.createElement("li");
this.passkeysHeadingElement.classList.add("inline-menu-list-heading");
this.passkeysHeadingElement.textContent = this.getTranslation("passkeys");
this.ciphersList.appendChild(this.passkeysHeadingElement);
return;
}
if (!this.passkeysHeadingElement || this.loginHeadingElement || passkeyData) {
return;
}
this.loginHeadingElement = globalThis.document.createElement("li");
this.loginHeadingElement.classList.add("inline-menu-list-heading");
this.loginHeadingElement.textContent = this.getTranslation("passwords");
this.ciphersList.appendChild(this.loginHeadingElement);
}
/**
* Builds the fill cipher button for a given cipher.
* Wraps the cipher icon and details.
@ -364,8 +527,13 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
fillCipherElement.classList.add("fill-cipher-button", "inline-menu-list-action");
fillCipherElement.setAttribute(
"aria-label",
`${this.getTranslation("fillCredentialsFor")} ${cipher.name}`,
`${
cipher.login?.passkey
? this.getTranslation("logInWithPasskey")
: this.getTranslation("fillCredentialsFor")
} ${cipher.name}`,
);
this.addFillCipherElementAriaDescription(fillCipherElement, cipher);
fillCipherElement.append(cipherIcon, cipherDetailsElement);
fillCipherElement.addEventListener(EVENTS.CLICK, this.handleFillCipherClickEvent(cipher));
@ -385,10 +553,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
cipher: InlineMenuCipherData,
) {
if (cipher.login) {
fillCipherElement.setAttribute(
"aria-description",
`${this.getTranslation("username")}: ${cipher.login.username}`,
);
const passkeyUserName = cipher.login.passkey?.userName || "";
const username = cipher.login.username || passkeyUserName;
if (username) {
fillCipherElement.setAttribute(
"aria-description",
`${this.getTranslation("username")}: ${username}`,
);
}
return;
}
@ -419,13 +591,15 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param cipher - The cipher to fill.
*/
private handleFillCipherClickEvent = (cipher: InlineMenuCipherData) => {
const usePasskey = !!cipher.login?.passkey;
return this.useEventHandlersMemo(
() =>
this.postMessageToParent({
command: "fillAutofillInlineMenuCipher",
inlineMenuCipherId: cipher.id,
usePasskey,
}),
`${cipher.id}-fill-cipher-button-click-handler`,
`${cipher.id}-fill-cipher-button-click-handler-${usePasskey ? "passkey" : ""}`,
);
};
@ -599,14 +773,20 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param cipher - The cipher to build the details for.
*/
private buildCipherDetailsElement(cipher: InlineMenuCipherData) {
const cipherNameElement = this.buildCipherNameElement(cipher);
const cipherSubtitleElement = this.buildCipherSubtitleElement(cipher);
const cipherDetailsElement = globalThis.document.createElement("span");
cipherDetailsElement.classList.add("cipher-details");
const cipherNameElement = this.buildCipherNameElement(cipher);
if (cipherNameElement) {
cipherDetailsElement.appendChild(cipherNameElement);
}
if (cipher.login?.passkey) {
return this.buildPasskeysCipherDetailsElement(cipher, cipherDetailsElement);
}
const subTitleText = this.getSubTitleText(cipher);
const cipherSubtitleElement = this.buildCipherSubtitleElement(subTitleText);
if (cipherSubtitleElement) {
cipherDetailsElement.appendChild(cipherSubtitleElement);
}
@ -635,10 +815,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
/**
* Builds the subtitle element for a given cipher.
*
* @param cipher - The cipher to build the username login element for.
* @param subTitleText - The subtitle text to display.
*/
private buildCipherSubtitleElement(cipher: InlineMenuCipherData): HTMLSpanElement | null {
const subTitleText = this.getSubTitleText(cipher);
private buildCipherSubtitleElement(subTitleText: string): HTMLSpanElement | null {
if (!subTitleText) {
return null;
}
@ -651,6 +830,52 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
return cipherSubtitleElement;
}
/**
* Builds the passkeys details for a given cipher. Includes the passkey name and username.
*
* @param cipher - The cipher to build the passkey details for.
* @param cipherDetailsElement - The cipher details element to append the passkey details to.
*/
private buildPasskeysCipherDetailsElement(
cipher: InlineMenuCipherData,
cipherDetailsElement: HTMLSpanElement,
): HTMLSpanElement {
let rpNameSubtitle: HTMLSpanElement;
if (cipher.name !== cipher.login.passkey.rpName) {
rpNameSubtitle = this.buildCipherSubtitleElement(cipher.login.passkey.rpName);
if (rpNameSubtitle) {
rpNameSubtitle.prepend(buildSvgDomElement(passkeyIcon));
rpNameSubtitle.classList.add("cipher-subtitle--passkey");
cipherDetailsElement.appendChild(rpNameSubtitle);
}
}
if (cipher.login.username) {
const usernameSubtitle = this.buildCipherSubtitleElement(cipher.login.username);
if (usernameSubtitle) {
if (!rpNameSubtitle) {
usernameSubtitle.prepend(buildSvgDomElement(passkeyIcon));
usernameSubtitle.classList.add("cipher-subtitle--passkey");
}
cipherDetailsElement.appendChild(usernameSubtitle);
}
return cipherDetailsElement;
}
const passkeySubtitle = this.buildCipherSubtitleElement(cipher.login.passkey.userName);
if (passkeySubtitle) {
if (!rpNameSubtitle) {
passkeySubtitle.prepend(buildSvgDomElement(passkeyIcon));
passkeySubtitle.classList.add("cipher-subtitle--passkey");
}
cipherDetailsElement.appendChild(passkeySubtitle);
}
return cipherDetailsElement;
}
/**
* Gets the subtitle text for a given cipher.
*
@ -779,7 +1004,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param currentListItem - The current list item.
*/
private focusNextListItem(currentListItem: HTMLElement) {
const nextListItem = currentListItem.nextSibling as HTMLElement;
let nextListItem = currentListItem.nextSibling as HTMLElement;
if (this.listItemIsHeadingElement(nextListItem)) {
nextListItem = nextListItem.nextSibling as HTMLElement;
}
const nextSibling = nextListItem?.querySelector(".inline-menu-list-action") as HTMLElement;
if (nextSibling) {
nextSibling.focus();
@ -791,7 +1020,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
return;
}
const firstListItem = currentListItem.parentElement?.firstChild as HTMLElement;
let firstListItem = currentListItem.parentElement?.firstChild as HTMLElement;
if (this.listItemIsHeadingElement(firstListItem)) {
firstListItem = firstListItem.nextSibling as HTMLElement;
}
const firstSibling = firstListItem?.querySelector(".inline-menu-list-action") as HTMLElement;
firstSibling?.focus();
}
@ -803,7 +1036,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param currentListItem - The current list item.
*/
private focusPreviousListItem(currentListItem: HTMLElement) {
const previousListItem = currentListItem.previousSibling as HTMLElement;
let previousListItem = currentListItem.previousSibling as HTMLElement;
if (this.listItemIsHeadingElement(previousListItem)) {
previousListItem = previousListItem.previousSibling as HTMLElement;
}
const previousSibling = previousListItem?.querySelector(
".inline-menu-list-action",
) as HTMLElement;
@ -856,4 +1093,13 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
private isFilledByIdentityCipher = () => {
return this.filledByCipherType === CipherType.Identity;
};
/**
* Identifies if the passed list item is a heading element.
*
* @param listItem - The list item to check.
*/
private listItemIsHeadingElement = (listItem: HTMLElement) => {
return listItem === this.passkeysHeadingElement || listItem === this.loginHeadingElement;
};
}

View File

@ -166,6 +166,35 @@ body {
}
}
.inline-menu-list-heading {
position: sticky;
top: 0;
z-index: 1;
font-family: $font-family-sans-serif;
font-weight: 600;
font-size: 1rem;
line-height: 1.3;
letter-spacing: 0.025rem;
width: 100%;
padding: 0.6rem 0.8rem;
will-change: transform;
border-bottom: 0.1rem solid;
@include themify($themes) {
color: themed("textColor");
background-color: themed("backgroundColor");
border-bottom-color: themed("backgroundColor");
}
&--bordered {
transition: border-bottom-color 0.15s ease;
@include themify($themes) {
border-bottom-color: themed("borderColor");
}
}
}
.inline-menu-list-container--with-new-item-button {
.inline-menu-list-actions {
max-height: 13.8rem;
@ -340,5 +369,28 @@ body {
@include themify($themes) {
color: themed("mutedTextColor");
}
&--passkey {
display: flex;
align-content: center;
align-items: center;
justify-content: flex-start;
svg {
width: 1.5rem;
height: 1.5rem;
margin-right: 0.2rem;
@include themify($themes) {
fill: themed("mutedTextColor") !important;
}
path {
@include themify($themes) {
fill: themed("mutedTextColor") !important;
}
}
}
}
}
}

View File

@ -20,9 +20,11 @@ export class InlineMenuFieldQualificationService
private usernameFieldTypes = new Set(["text", "email", "number", "tel"]);
private usernameAutocompleteValue = "username";
private emailAutocompleteValue = "email";
private webAuthnAutocompleteValue = "webauthn";
private loginUsernameAutocompleteValues = new Set([
this.usernameAutocompleteValue,
this.emailAutocompleteValue,
this.webAuthnAutocompleteValue,
]);
private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(",");

View File

@ -187,7 +187,10 @@ export function createAutofillOverlayCipherDataMock(
return {
id: String(index),
name: `website login ${index}`,
login: { username: `username${index}` },
login: {
username: `username${index}`,
passkey: null,
},
type: CipherType.Login,
reprompt: CipherRepromptType.None,
favorite: false,

View File

@ -1,19 +1,20 @@
const logoIcon =
export const logoIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none"><path fill="#175DDC" d="M12.66.175A.566.566 0 0 0 12.25 0H1.75a.559.559 0 0 0-.409.175.561.561 0 0 0-.175.41v7c.002.532.105 1.06.305 1.554.189.488.444.948.756 1.368.322.42.682.81 1.076 1.163.365.335.75.649 1.152.939.35.248.718.483 1.103.706.385.222.656.372.815.45.16.08.29.141.386.182A.53.53 0 0 0 7 14a.509.509 0 0 0 .238-.055c.098-.043.225-.104.387-.182.162-.079.438-.23.816-.45.378-.222.75-.459 1.102-.707.403-.29.788-.604 1.154-.939a8.435 8.435 0 0 0 1.076-1.163c.312-.42.567-.88.757-1.367a4.19 4.19 0 0 0 .304-1.555v-7a.55.55 0 0 0-.174-.407Z"/><path fill="#fff" d="M7 12.365s4.306-2.18 4.306-4.717V1.5H7v10.865Z"/></svg>';
const logoLockedIcon =
export const logoLockedIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" d="M12.66.175A.566.566 0 0 0 12.25 0H1.75a.559.559 0 0 0-.409.175.561.561 0 0 0-.175.41v7c.002.532.105 1.06.305 1.554.189.488.444.948.756 1.368.322.42.682.81 1.076 1.163.365.335.75.649 1.152.939.35.248.718.483 1.103.706.385.222.656.372.815.45.16.08.29.141.386.182A.53.53 0 0 0 7 14a.509.509 0 0 0 .238-.055c.098-.043.225-.104.387-.182.162-.079.438-.23.816-.45.378-.222.75-.459 1.102-.707.403-.29.788-.604 1.154-.939a8.435 8.435 0 0 0 1.076-1.163c.312-.42.567-.88.757-1.367a4.19 4.19 0 0 0 .304-1.555v-7a.55.55 0 0 0-.174-.407Z"/><path fill="#fff" d="M7 12.365s4.306-2.18 4.306-4.717V1.5H7v10.865Z"/><circle cx="12.889" cy="12.889" r="4.889" fill="#F8F9FA"/><path fill="#555" d="M11.26 11.717h2.37v-.848c0-.313-.116-.58-.348-.8a1.17 1.17 0 0 0-.838-.332c-.327 0-.606.11-.838.332a1.066 1.066 0 0 0-.347.8v.848Zm3.851.424v2.546a.4.4 0 0 1-.13.3.44.44 0 0 1-.314.124h-4.445a.44.44 0 0 1-.315-.124.4.4 0 0 1-.13-.3V12.14a.4.4 0 0 1 .13-.3.44.44 0 0 1 .315-.124h.148v-.848c0-.542.204-1.008.612-1.397a2.044 2.044 0 0 1 1.462-.583c.568 0 1.056.194 1.463.583.408.39.611.855.611 1.397v.848h.149a.44.44 0 0 1 .315.124.4.4 0 0 1 .13.3Z"/></g><defs><clipPath id="a"><rect width="16" height="16" fill="#fff" rx="2"/></clipPath></defs></svg>';
const globeIcon =
export const globeIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"><path fill="#777" fill-rule="evenodd" d="M18.026 17.842c-1.418 1.739-3.517 2.84-5.86 2.84a7.364 7.364 0 0 1-3.431-.848l.062-.15.062-.151.063-.157c.081-.203.17-.426.275-.646.133-.28.275-.522.426-.68.026-.028.101-.075.275-.115.165-.037.376-.059.629-.073.138-.008.288-.014.447-.02.399-.016.847-.034 1.266-.092.314-.044.566-.131.755-.271a.884.884 0 0 0 .352-.555c.037-.2.008-.392-.03-.543-.035-.135-.084-.264-.12-.355l-.01-.03a4.26 4.26 0 0 0-.145-.33c-.126-.264-.237-.497-.288-1.085-.03-.344.09-.73.251-1.138l.089-.22c.05-.123.1-.247.14-.355.064-.171.129-.375.129-.566a1.51 1.51 0 0 0-.134-.569 2.573 2.573 0 0 0-.319-.547c-.246-.323-.635-.669-1.093-.669-.44 0-1.006.169-1.487.368-.246.102-.48.216-.68.33-.192.111-.372.235-.492.359-.93.96-1.48 1.239-1.81 1.258-.277.017-.478-.15-.736-.525a9.738 9.738 0 0 1-.19-.29l-.006-.01a11.568 11.568 0 0 0-.198-.305 2.76 2.76 0 0 0-.521-.6 1.39 1.39 0 0 0-1.088-.314 8.302 8.302 0 0 1 1.987-3.936c.055.342.146.626.272.856.23.42.561.64.926.716.406.086.857-.061 1.26-.216.125-.047.248-.097.372-.147.309-.125.618-.25.947-.341.26-.072.581-.057.959.012.264.049.529.118.8.19l.36.091c.379.094.782.178 1.135.148.374-.032.733-.197.934-.623a.874.874 0 0 0 .024-.752c-.087-.197-.24-.355-.35-.47-.26-.267-.412-.427-.412-.685 0-.125.037-.2.09-.263a.982.982 0 0 1 .303-.211c.059-.03.119-.058.183-.089l.036-.016a3.79 3.79 0 0 0 .236-.118c.047-.026.098-.056.148-.093 1.936.747 3.51 2.287 4.368 4.249a7.739 7.739 0 0 0-.031-.004c-.38-.047-.738-.056-1.063.061-.34.123-.603.368-.817.74-.122.211-.284.43-.463.67l-.095.129c-.207.281-.431.595-.58.92-.15.326-.245.705-.142 1.103.104.397.387.738.837 1.036.099.065.225.112.314.145l.02.008c.108.04.195.074.268.117.07.042.106.08.124.114.017.03.037.087.022.206-.047.376-.069.73-.052 1.034.017.292.071.59.218.809.118.174.12.421.108.786v.01a2.46 2.46 0 0 0 .021.518.809.809 0 0 0 .15.35Zm1.357.059a9.654 9.654 0 0 0 1.62-5.386c0-5.155-3.957-9.334-8.836-9.334-4.88 0-8.836 4.179-8.836 9.334 0 3.495 1.82 6.543 4.513 8.142v.093h.161a8.426 8.426 0 0 0 4.162 1.098c2.953 0 5.568-1.53 7.172-3.882a.569.569 0 0 0 .048-.062l-.004-.003ZM8.152 19.495a43.345 43.345 0 0 1 .098-.238l.057-.142c.082-.205.182-.455.297-.698.143-.301.323-.624.552-.864.163-.172.392-.254.602-.302.219-.05.473-.073.732-.088.162-.01.328-.016.495-.023.386-.015.782-.03 1.168-.084.255-.036.392-.099.461-.15.06-.045.076-.084.083-.12a.534.534 0 0 0-.02-.223 2.552 2.552 0 0 0-.095-.278l-.01-.027a3.128 3.128 0 0 0-.104-.232c-.134-.282-.31-.65-.374-1.381-.046-.533.138-1.063.3-1.472.035-.09.069-.172.1-.249.046-.11.086-.21.123-.31.062-.169.083-.264.083-.312a.812.812 0 0 0-.076-.283 1.867 1.867 0 0 0-.23-.394c-.21-.274-.428-.408-.577-.408-.315 0-.788.13-1.246.32a5.292 5.292 0 0 0-.603.293 1.727 1.727 0 0 0-.347.244c-.936.968-1.641 1.421-2.235 1.457-.646.04-1.036-.413-1.31-.813-.07-.103-.139-.21-.203-.311l-.005-.007c-.064-.101-.125-.197-.188-.29a2.098 2.098 0 0 0-.387-.453.748.748 0 0 0-.436-.18c-.1-.006-.22.005-.365.046a8.707 8.707 0 0 0-.056.992c0 2.957 1.488 5.547 3.716 6.98Zm10.362-2.316.003-.192.002-.046c.01-.305.026-.786-.232-1.169-.036-.054-.082-.189-.096-.444-.014-.243.003-.55.047-.9a1.051 1.051 0 0 0-.105-.649.987.987 0 0 0-.374-.374 2.285 2.285 0 0 0-.367-.166h-.003a1.243 1.243 0 0 1-.205-.088c-.369-.244-.505-.46-.549-.629-.044-.168-.015-.364.099-.61.115-.25.297-.511.507-.796l.089-.12c.178-.239.368-.495.512-.745.152-.263.302-.382.466-.441.18-.065.416-.073.77-.03.142.018.275.04.397.063.274.837.423 1.736.423 2.671a8.45 8.45 0 0 1-1.384 4.665Zm-4.632-12.63a7.362 7.362 0 0 0-1.715-.201c-1.89 0-3.621.716-4.965 1.905.025.54.12.887.24 1.105.13.238.295.34.482.38.2.042.484-.027.905-.188l.328-.13c.32-.13.681-.275 1.048-.377.398-.111.833-.075 1.24 0 .289.053.59.132.871.205l.326.084c.383.094.694.151.932.13.216-.017.326-.092.395-.237.039-.083.027-.114.014-.144-.027-.062-.088-.136-.212-.264l-.043-.044c-.218-.222-.567-.578-.567-1.142 0-.305.101-.547.262-.734.137-.159.308-.267.46-.347Z" clip-rule="evenodd"/></svg>';
const lockIcon =
export const lockIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" d="M8.799 11.633a.68.68 0 0 0-.639.422.695.695 0 0 0-.054.264.682.682 0 0 0 .374.6v1.13a.345.345 0 1 0 .693 0v-1.17a.68.68 0 0 0 .315-.56.695.695 0 0 0-.204-.486.682.682 0 0 0-.485-.2Zm4.554-4.657h-7.11a.438.438 0 0 1-.406-.26A3.81 3.81 0 0 1 5.584 4.3c.112-.435.312-.842.588-1.195A3.196 3.196 0 0 1 7.19 2.25a3.468 3.468 0 0 1 3.225-.059A3.62 3.62 0 0 1 11.94 3.71l.327.59a.502.502 0 1 0 .885-.483l-.307-.552a4.689 4.689 0 0 0-2.209-2.078 4.466 4.466 0 0 0-3.936.185A4.197 4.197 0 0 0 5.37 2.49a4.234 4.234 0 0 0-.768 1.565 4.714 4.714 0 0 0 .162 2.682.182.182 0 0 1-.085.22.173.173 0 0 1-.082.02h-.353a1.368 1.368 0 0 0-1.277.842c-.07.168-.107.348-.109.53v7.1a1.392 1.392 0 0 0 .412.974 1.352 1.352 0 0 0 .974.394h9.117c.363.001.711-.142.97-.4a1.39 1.39 0 0 0 .407-.972v-7.1a1.397 1.397 0 0 0-.414-.973 1.368 1.368 0 0 0-.972-.396Zm.37 8.469a.373.373 0 0 1-.11.26.364.364 0 0 1-.26.107H4.246a.366.366 0 0 1-.26-.107.374.374 0 0 1-.11-.261V8.349a.374.374 0 0 1 .11-.26.366.366 0 0 1 .26-.108h9.116a.366.366 0 0 1 .37.367l-.008 7.097Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M.798.817h16v16h-16z"/></clipPath></defs></svg>';
const plusIcon =
export const plusIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" fill-rule="evenodd" d="M9.607 7.15h5.35c.322 0 .627.133.847.362a1.213 1.213 0 0 1 .002 1.68c-.221.23-.527.363-.85.363H9.607v5.652c0 .312-.12.613-.336.839a1.176 1.176 0 0 1-1.696.003 1.21 1.21 0 0 1-.34-.842V9.555H1.888a1.173 1.173 0 0 1-.847-.361A1.193 1.193 0 0 1 .7 8.352a1.219 1.219 0 0 1 .336-.838 1.175 1.175 0 0 1 .85-.364h5.349V1.635c0-.31.118-.611.336-.84A1.176 1.176 0 0 1 9.268.795c.222.228.34.533.34.841V7.15Z" clip-rule="evenodd"/></g><defs><clipPath id="a"><path fill="#fff" d="M.421.421h16v16h-16z"/></clipPath></defs></svg>';
const viewCipherIcon =
export const viewCipherIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><g clip-path="url(#a)"><path fill="#175DDC" d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .113h20v19.773H0z"/></clipPath></defs></svg>';
export { logoIcon, logoLockedIcon, globeIcon, lockIcon, plusIcon, viewCipherIcon };
export const passkeyIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="15" viewBox="0 0 14 15" fill="none"><path fill="#6D757E" d="M2.35 12.112a.713.713 0 1 1 0-1.426.713.713 0 0 1 0 1.426Z"/><path fill="#6D757E" fill-rule="evenodd" d="M4.597 7.695a3.5 3.5 0 1 1 3.741 0A5.33 5.33 0 0 1 10.5 9.186c.154.172.29.328.384.461l1.562-.001L14 11.14l-2.188 1.952-.874-.875-.876.875-.874-.875-.876.84-2.613-.003a3.152 3.152 0 0 1-2.634 1.307c-1.729-.036-3.101-1.436-3.064-3.127C.038 9.543 1.469 8.2 3.199 8.237c.098.002.195.009.291.02a6.76 6.76 0 0 1 .296-.181c.257-.149.528-.276.81-.381Zm1.176 1.957 3.952-.004a4.11 4.11 0 0 0-.498-.462 4.452 4.452 0 0 0-2.76-.95c-.647 0-1.262.137-1.817.384a3.12 3.12 0 0 1 1.123 1.032Zm-1.93-4.916a2.625 2.625 0 1 0 5.25 0 2.625 2.625 0 0 0-5.25 0Zm1.407 7.442-.262.366a2.277 2.277 0 0 1-1.904.942C1.819 13.459.85 12.442.876 11.253c.025-1.19 1.04-2.168 2.304-2.141a2.27 2.27 0 0 1 1.86 1.019l.26.396 6.794-.006.619.595-.866.773-.91-.909-.874.875-.863-.862-1.239 1.19-2.711-.005Z" clip-rule="evenodd"/></svg>';

View File

@ -116,6 +116,7 @@ import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/ser
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/services/fido2/fido2-active-request-manager";
import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service";
import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
@ -984,6 +985,7 @@ export default class MainBackground {
this.syncService,
this.logService,
);
const fido2ActiveRequestManager = new Fido2ActiveRequestManager();
this.fido2ClientService = new Fido2ClientService(
this.fido2AuthenticatorService,
this.configService,
@ -991,6 +993,7 @@ export default class MainBackground {
this.vaultSettingsService,
this.domainSettingsService,
this.taskSchedulerService,
fido2ActiveRequestManager,
this.logService,
);
@ -1536,6 +1539,7 @@ export default class MainBackground {
this.autofillSettingsService,
this.i18nService,
this.platformUtilsService,
this.fido2ClientService,
this.themeStateService,
);
}

View File

@ -111,6 +111,7 @@ export class WebauthnUtils {
rpId: keyOptions.rpId,
userVerification: keyOptions.userVerification,
timeout: keyOptions.timeout,
mediation: options.mediation,
fallbackSupported,
};
}

View File

@ -56,6 +56,8 @@ export const NOTIFICATION_BAR_LIFESPAN_MS = 150000; // 150 seconds
export const AUTOFILL_OVERLAY_HANDLE_REPOSITION = "autofill-overlay-handle-reposition-event";
export const UPDATE_PASSKEYS_HEADINGS_ON_SCROLL = "update-passkeys-headings-on-scroll";
export const AutofillOverlayVisibility = {
Off: 0,
OnButtonClick: 1,

View File

@ -0,0 +1,21 @@
import { Observable, Subject } from "rxjs";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
export interface ActiveRequest {
credentials: Fido2CredentialView[];
subject: Subject<string>;
}
export type RequestCollection = Readonly<{ [tabId: number]: ActiveRequest }>;
export abstract class Fido2ActiveRequestManager {
getActiveRequest$: (tabId: number) => Observable<ActiveRequest | undefined>;
getActiveRequest: (tabId: number) => ActiveRequest | undefined;
newActiveRequest: (
tabId: number,
credentials: Fido2CredentialView[],
abortController: AbortController,
) => Promise<string>;
removeActiveRequest: (tabId: number) => void;
}

View File

@ -1,3 +1,5 @@
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
/**
* This class represents an abstraction of the WebAuthn Authenticator model as described by W3C:
* https://www.w3.org/TR/webauthn-3/#sctn-authenticator-model
@ -32,6 +34,14 @@ export abstract class Fido2AuthenticatorService {
tab: chrome.tabs.Tab,
abortController?: AbortController,
) => Promise<Fido2AuthenticatorGetAssertionResult>;
/**
* Discover credentials for a given Relying Party
*
* @param rpId The Relying Party's ID
* @returns A promise that resolves with an array of discoverable credentials
*/
silentCredentialDiscovery: (rpId: string) => Promise<Fido2CredentialView[]>;
}
export enum Fido2AlgorithmIdentifier {
@ -132,6 +142,9 @@ export interface Fido2AuthenticatorGetAssertionParams {
extensions: unknown;
/** Forwarded to user interface */
fallbackSupported: boolean;
// Bypass the UI and assume that the user has already interacted with the authenticator
assumeUserPresence?: boolean;
}
export interface Fido2AuthenticatorGetAssertionResult {

View File

@ -1,3 +1,7 @@
import { Observable } from "rxjs";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
export const UserRequestedFallbackAbortReason = "UserRequestedFallback";
export type UserVerification = "discouraged" | "preferred" | "required";
@ -16,6 +20,10 @@ export type UserVerification = "discouraged" | "preferred" | "required";
export abstract class Fido2ClientService {
isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>;
availableAutofillCredentials$: (tabId: number) => Observable<Fido2CredentialView[]>;
autofillCredential: (tabId: number, credentialId: string) => Promise<void>;
/**
* Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source.
* For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential
@ -142,6 +150,7 @@ export interface AssertCredentialParams {
userVerification?: UserVerification;
timeout: number;
sameOriginWithAncestors: boolean;
mediation?: "silent" | "optional" | "required" | "conditional";
fallbackSupported: boolean;
}

View File

@ -0,0 +1,89 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom, Observable } from "rxjs";
import { flushPromises } from "@bitwarden/browser/src/autofill/spec/testing-utils";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import { Fido2ActiveRequestManager } from "./fido2-active-request-manager";
jest.mock("rxjs", () => {
const rxjs = jest.requireActual("rxjs");
const { firstValueFrom } = rxjs;
return {
...rxjs,
firstValueFrom: jest.fn(firstValueFrom),
};
});
describe("Fido2ActiveRequestManager", () => {
const credentialId = "123";
const tabId = 1;
let requestManager: Fido2ActiveRequestManager;
beforeEach(() => {
requestManager = new Fido2ActiveRequestManager();
});
afterEach(() => {
jest.clearAllMocks();
});
it("creates a new active request", async () => {
const fido2CredentialView = mock<Fido2CredentialView>({
credentialId,
});
const credentials = [fido2CredentialView];
const abortController = new AbortController();
(firstValueFrom as jest.Mock).mockResolvedValue(credentialId);
const result = await requestManager.newActiveRequest(tabId, credentials, abortController);
await flushPromises();
expect(result).toBe(credentialId);
});
it("gets the observable stream of active requests", async () => {
(firstValueFrom as jest.Mock).mockResolvedValue(credentialId);
await requestManager.newActiveRequest(tabId, [], new AbortController());
const result = requestManager.getActiveRequest$(tabId);
expect(result).toBeInstanceOf(Observable);
result.subscribe((activeRequest) => {
expect(activeRequest).toBeDefined();
});
});
it("returns the active request associated with a given tab id", async () => {
const fido2CredentialView = mock<Fido2CredentialView>({
credentialId,
});
const credentials = [fido2CredentialView];
(firstValueFrom as jest.Mock).mockResolvedValue(credentialId);
await requestManager.newActiveRequest(tabId, credentials, new AbortController());
const result = requestManager.getActiveRequest(tabId);
expect(result).toEqual({
credentials: credentials,
subject: expect.any(Object),
});
});
it("removes the active request associated with a given tab id", async () => {
const fido2CredentialView = mock<Fido2CredentialView>({
credentialId,
});
const credentials = [fido2CredentialView];
(firstValueFrom as jest.Mock).mockResolvedValue(credentialId);
await requestManager.newActiveRequest(tabId, credentials, new AbortController());
requestManager.removeActiveRequest(tabId);
const result = requestManager.getActiveRequest(tabId);
expect(result).toBeUndefined();
});
});

View File

@ -0,0 +1,109 @@
import {
BehaviorSubject,
distinctUntilChanged,
firstValueFrom,
map,
Observable,
shareReplay,
startWith,
Subject,
} from "rxjs";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import {
ActiveRequest,
RequestCollection,
Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction,
} from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstraction {
private activeRequests$: BehaviorSubject<RequestCollection> = new BehaviorSubject({});
/**
* Gets the observable stream of all active requests associated with a given tab id.
*
* @param tabId - The tab id to get the active request for.
*/
getActiveRequest$(tabId: number): Observable<ActiveRequest | undefined> {
return this.activeRequests$.pipe(
map((requests) => requests[tabId]),
distinctUntilChanged(),
shareReplay({ bufferSize: 1, refCount: true }),
startWith(undefined),
);
}
/**
* Gets the active request associated with a given tab id.
*
* @param tabId - The tab id to get the active request for.
*/
getActiveRequest(tabId: number): ActiveRequest | undefined {
return this.activeRequests$.value[tabId];
}
/**
* Creates a new active fido2 request.
*
* @param tabId - The tab id to associate the request with.
* @param credentials - The credentials to use for the request.
* @param abortController - The abort controller to use for the request.
*/
async newActiveRequest(
tabId: number,
credentials: Fido2CredentialView[],
abortController: AbortController,
): Promise<string> {
const newRequest: ActiveRequest = {
credentials,
subject: new Subject(),
};
this.updateRequests((existingRequests) => ({
...existingRequests,
[tabId]: newRequest,
}));
const abortListener = () => this.abortActiveRequest(tabId);
abortController.signal.addEventListener("abort", abortListener);
const credentialId = firstValueFrom(newRequest.subject);
abortController.signal.removeEventListener("abort", abortListener);
return credentialId;
}
/**
* Removes and aborts the active request associated with a given tab id.
*
* @param tabId - The tab id to abort the active request for.
*/
removeActiveRequest(tabId: number) {
this.abortActiveRequest(tabId);
this.updateRequests((existingRequests) => {
const newRequests = { ...existingRequests };
delete newRequests[tabId];
return newRequests;
});
}
/**
* Aborts the active request associated with a given tab id.
*
* @param tabId - The tab id to abort the active request for.
*/
private abortActiveRequest(tabId: number): void {
this.activeRequests$.value[tabId]?.subject.error(
new DOMException("The operation either timed out or was not allowed.", "AbortError"),
);
}
/**
* Updates the active requests.
*
* @param updateFunction - The function to use to update the active requests.
*/
private updateRequests(
updateFunction: (existingRequests: RequestCollection) => RequestCollection,
) {
this.activeRequests$.next(updateFunction(this.activeRequests$.value));
}
}

View File

@ -756,6 +756,22 @@ describe("FidoAuthenticatorService", () => {
});
});
describe("silentCredentialDiscovery", () => {
it("returns the fido2Credentials of a cipher found by its rpId", async () => {
const credentialId = Utils.newGuid();
const cipher = await createCipherView(
{ type: CipherType.Login },
{ credentialId, rpId: RpId, discoverable: true },
);
const ciphers = [cipher];
cipherService.getAllDecrypted.mockResolvedValue(ciphers);
const result = await authenticator.silentCredentialDiscovery(RpId);
expect(result).toEqual([cipher.login.fido2Credentials[0]]);
});
});
async function createParams(
params: Partial<Fido2AuthenticatorGetAssertionParams> = {},
): Promise<Fido2AuthenticatorGetAssertionParams> {

View File

@ -234,10 +234,15 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.NotAllowed);
}
const response = await userInterfaceSession.pickCredential({
cipherIds: cipherOptions.map((cipher) => cipher.id),
userVerification: params.requireUserVerification,
});
let response;
if (this.requiresUserVerificationPrompt(params, cipherOptions)) {
response = await userInterfaceSession.pickCredential({
cipherIds: cipherOptions.map((cipher) => cipher.id),
userVerification: params.requireUserVerification,
});
} else {
response = { cipherId: cipherOptions[0].id, userVerified: false };
}
const selectedCipherId = response.cipherId;
const userVerified = response.userVerified;
const selectedCipher = cipherOptions.find((c) => c.id === selectedCipherId);
@ -310,6 +315,24 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
}
}
private requiresUserVerificationPrompt(
params: Fido2AuthenticatorGetAssertionParams,
cipherOptions: CipherView[],
): boolean {
return (
params.requireUserVerification ||
!params.assumeUserPresence ||
cipherOptions.length > 1 ||
cipherOptions.length === 0 ||
cipherOptions.some((cipher) => cipher.reprompt !== CipherRepromptType.None)
);
}
async silentCredentialDiscovery(rpId: string): Promise<Fido2CredentialView[]> {
const credentials = await this.findCredentialsByRp(rpId);
return credentials.map((c) => c.login.fido2Credentials[0]);
}
/** Finds existing crendetials and returns the `cipherId` for each one */
private async findExcludedCredentials(
credentials: PublicKeyCredentialDescriptor[],

View File

@ -1,12 +1,17 @@
import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { BehaviorSubject, of } from "rxjs";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { Utils } from "../../../platform/misc/utils";
import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import { ConfigService } from "../../abstractions/config/config.service";
import {
ActiveRequest,
Fido2ActiveRequestManager,
} from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
import {
Fido2AuthenticatorError,
Fido2AuthenticatorErrorCode,
@ -37,6 +42,8 @@ describe("FidoAuthenticatorService", () => {
let vaultSettingsService: MockProxy<VaultSettingsService>;
let domainSettingsService: MockProxy<DomainSettingsService>;
let taskSchedulerService: MockProxy<TaskSchedulerService>;
let activeRequest!: MockProxy<ActiveRequest>;
let requestManager!: MockProxy<Fido2ActiveRequestManager>;
let client!: Fido2ClientService;
let tab!: chrome.tabs.Tab;
let isValidRpId!: jest.SpyInstance;
@ -48,6 +55,13 @@ describe("FidoAuthenticatorService", () => {
vaultSettingsService = mock<VaultSettingsService>();
domainSettingsService = mock<DomainSettingsService>();
taskSchedulerService = mock<TaskSchedulerService>();
activeRequest = mock<ActiveRequest>({
subject: new BehaviorSubject<string>(""),
});
requestManager = mock<Fido2ActiveRequestManager>({
getActiveRequest$: (tabId: number) => new BehaviorSubject(activeRequest),
getActiveRequest: (tabId: number) => activeRequest,
});
isValidRpId = jest.spyOn(DomainUtils, "isValidRpId");
@ -58,11 +72,12 @@ describe("FidoAuthenticatorService", () => {
vaultSettingsService,
domainSettingsService,
taskSchedulerService,
requestManager,
);
configService.serverConfig$ = of({ environment: { vault: VaultUrl } } as any);
vaultSettingsService.enablePasskeys$ = of(true);
domainSettingsService.neverDomains$ = of({});
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
});
@ -592,6 +607,50 @@ describe("FidoAuthenticatorService", () => {
});
});
describe("assert mediated conditional ui credential", () => {
const params = createParams({
userVerification: "required",
mediation: "conditional",
allowedCredentialIds: [],
});
beforeEach(() => {
requestManager.newActiveRequest.mockResolvedValue(crypto.randomUUID());
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
});
it("creates an active mediated conditional request", async () => {
await client.assertCredential(params, tab);
expect(requestManager.newActiveRequest).toHaveBeenCalled();
expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({
assumeUserPresence: true,
rpId: RpId,
}),
tab,
);
});
it("restarts the mediated conditional request if a user aborts the request", async () => {
authenticator.getAssertion.mockRejectedValueOnce(new Error());
await client.assertCredential(params, tab);
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
});
it("restarts the mediated conditional request if a the abort controller aborts the request", async () => {
const abortController = new AbortController();
abortController.abort();
authenticator.getAssertion.mockRejectedValueOnce(new DOMException("AbortError"));
await client.assertCredential(params, tab);
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
});
});
function createParams(params: Partial<AssertCredentialParams> = {}): AssertCredentialParams {
return {
allowedCredentialIds: params.allowedCredentialIds ?? [],
@ -602,6 +661,7 @@ describe("FidoAuthenticatorService", () => {
userVerification: params.userVerification,
sameOriginWithAncestors: true,
fallbackSupported: params.fallbackSupported ?? false,
mediation: params.mediation,
};
}
@ -616,6 +676,28 @@ describe("FidoAuthenticatorService", () => {
};
}
});
describe("autofill of credentials through the active request manager", () => {
it("returns an observable that updates with an array of the credentials for active Fido2 requests", async () => {
const activeRequestCredentials = mock<Fido2CredentialView>();
activeRequest.credentials = [activeRequestCredentials];
const observable = client.availableAutofillCredentials$(tab.id);
observable.subscribe((credentials) => {
expect(credentials).toEqual([activeRequestCredentials]);
});
});
it("triggers the logic of the next behavior subject of an active request", async () => {
const activeRequestCredentials = mock<Fido2CredentialView>();
activeRequest.credentials = [activeRequestCredentials];
jest.spyOn(activeRequest.subject, "next");
await client.autofillCredential(tab.id, activeRequestCredentials.credentialId);
expect(activeRequest.subject.next).toHaveBeenCalled();
});
});
});
/** This is a fake function that always returns the same byte sequence */

View File

@ -1,15 +1,18 @@
import { firstValueFrom, Subscription } from "rxjs";
import { firstValueFrom, map, Observable, Subscription } from "rxjs";
import { parse } from "tldts";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import { ConfigService } from "../../abstractions/config/config.service";
import { Fido2ActiveRequestManager } from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
import {
Fido2AuthenticatorError,
Fido2AuthenticatorErrorCode,
Fido2AuthenticatorGetAssertionParams,
Fido2AuthenticatorGetAssertionResult,
Fido2AuthenticatorMakeCredentialsParams,
Fido2AuthenticatorService,
PublicKeyCredentialDescriptor,
@ -32,6 +35,7 @@ import { TaskSchedulerService } from "../../scheduling/task-scheduler.service";
import { isValidRpId } from "./domain-utils";
import { Fido2Utils } from "./fido2-utils";
import { guidToRawFormat } from "./guid-utils";
/**
* Bitwarden implementation of the Web Authentication API as described by W3C
@ -61,6 +65,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
private vaultSettingsService: VaultSettingsService,
private domainSettingsService: DomainSettingsService,
private taskSchedulerService: TaskSchedulerService,
private requestManager: Fido2ActiveRequestManager,
private logService?: LogService,
) {
this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.fido2ClientAbortTimeout, () =>
@ -68,6 +73,17 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
);
}
availableAutofillCredentials$(tabId: number): Observable<Fido2CredentialView[]> {
return this.requestManager
.getActiveRequest$(tabId)
.pipe(map((request) => request?.credentials ?? []));
}
async autofillCredential(tabId: number, credentialId: string) {
const request = this.requestManager.getActiveRequest(tabId);
request.subject.next(credentialId);
}
async isFido2FeatureEnabled(hostname: string, origin: string): Promise<boolean> {
const isUserLoggedIn =
(await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut;
@ -287,6 +303,16 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
};
const clientDataJSON = JSON.stringify(collectedClientData);
const clientDataJSONBytes = Utils.fromByteStringToArray(clientDataJSON);
if (params.mediation === "conditional") {
return this.handleMediatedConditionalRequest(
params,
tab,
abortController,
clientDataJSONBytes,
);
}
const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, clientDataJSONBytes);
const getAssertionParams = mapToGetAssertionParams({ params, clientDataHash });
@ -339,6 +365,59 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
timeoutSubscription?.unsubscribe();
return this.generateAssertCredentialResult(getAssertionResult, clientDataJSONBytes);
}
private async handleMediatedConditionalRequest(
params: AssertCredentialParams,
tab: chrome.tabs.Tab,
abortController: AbortController,
clientDataJSONBytes: Uint8Array,
): Promise<AssertCredentialResult> {
let getAssertionResult;
let assumeUserPresence = false;
while (!getAssertionResult) {
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
const availableCredentials =
authStatus === AuthenticationStatus.Unlocked
? await this.authenticator.silentCredentialDiscovery(params.rpId)
: [];
this.logService?.info(
`[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`,
);
const credentialId = await this.requestManager.newActiveRequest(
tab.id,
availableCredentials,
abortController,
);
params.allowedCredentialIds = [Fido2Utils.bufferToString(guidToRawFormat(credentialId))];
assumeUserPresence = true;
const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, clientDataJSONBytes);
const getAssertionParams = mapToGetAssertionParams({
params,
clientDataHash,
assumeUserPresence,
});
try {
getAssertionResult = await this.authenticator.getAssertion(getAssertionParams, tab);
} catch (e) {
this.logService?.info(`[Fido2Client] Aborted by user: ${e}`);
}
if (abortController.signal.aborted) {
this.logService?.info(`[Fido2Client] Aborted with AbortController`);
}
}
return this.generateAssertCredentialResult(getAssertionResult, clientDataJSONBytes);
}
private generateAssertCredentialResult(
getAssertionResult: Fido2AuthenticatorGetAssertionResult,
clientDataJSONBytes: Uint8Array,
): AssertCredentialResult {
return {
authenticatorData: Fido2Utils.bufferToString(getAssertionResult.authenticatorData),
clientDataJSON: Fido2Utils.bufferToString(clientDataJSONBytes),
@ -431,9 +510,11 @@ function mapToMakeCredentialParams({
function mapToGetAssertionParams({
params,
clientDataHash,
assumeUserPresence,
}: {
params: AssertCredentialParams;
clientDataHash: ArrayBuffer;
assumeUserPresence?: boolean;
}): Fido2AuthenticatorGetAssertionParams {
const allowCredentialDescriptorList: PublicKeyCredentialDescriptor[] =
params.allowedCredentialIds.map((id) => ({
@ -453,5 +534,6 @@ function mapToGetAssertionParams({
allowCredentialDescriptorList,
extensions: {},
fallbackSupported: params.fallbackSupported,
assumeUserPresence,
};
}