1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

[PM-3812][PM-3809] Unify Create and Login Passkeys UI (#6403)

* PM-1235 Added component to display passkey on auth flow

* PM-1235 Implement basic structure and behaviour of UI

* PM-1235 Added localised strings

* PM-1235 Improved button UI

* Implemented view passkey button

* Implemented multiple matching passkeys

* Refactored fido2 popup to use browser popout windows service

* [PM-3807] feat: remove non-discoverable from fido2 user interface class

* [PM-3807] feat: merge fido2 component ui

* [PM-3807] feat: return `cipherId` from user interface

* [PM-3807] feat: merge credential creation logic in authenticator

* [PM-3807] feat: merge credential assertion logic in authenticator

* updated test cases and services using the config service

* [PM-3807] feat: add `discoverable` property to fido2keys

* [PM-3807] feat: assign discoverable property during creation

* [PM-3807] feat: save discoverable field to server

* [PM-3807] feat: filter credentials by rpId AND discoverable

* [PM-3807] chore: remove discoverable tests which are no longer needed

* [PM-3807] chore: remove all logic for handling standalone Fido2Key

View and components will be cleaned up as part of UI tickets

* [PM-3807] fix: add missing discoverable property handling to tests

* updated locales with new text

* Updated popout windows service to use defined type for custom width and height

* Update on unifying auth flow ui to align with architecture changes

* Moved click event

* Throw dom exception error if tab is null

* updated fido2key object to array

* removed discoverable key in client inerface service for now

* Get senderTabId from the query params and send to the view cipher component to allow the pop out close when the close button is clicked on the view cipher component

* Refactored view item if passkeys exists and the cipher row views by having an extra ng-conatiner for each case

* Allow fido2 pop out close wehn cancle is clicked on add edit component

* Removed makshift run in angular zone

* created focus directive to target first element in ngFor for displayed ciphers in fido2

* Refactored to use switch statement and added condtional on search and add div

* Adjusted footer link and added more features to the login flow

* Added host listener to abort when window is closed

* remove custom focus directive. instead stuck focus logic into fido2-cipher-row component

* Fixed bug where close and cancel on view and add component does not abort the fido2 request

* show info dialog when user account does not have master password

* Removed PopupUtilsService

* show info dialog when user account does not have master password

* Added comments

* Added comments

* made row height consistent

* update logo to be dynamic with theme selection

* added new translation key

* Dis some styling to align cipher items

* Changed passkey icon fill color

* updated flow of focus and selected items in the passkey popup

* Fixed bug when picking a credential

* Added text to lock popout screen

* Added passkeys test to home view

* changed class name

* Added uilocation as a query paramter to know if the user is in the popout window

* update fido2 component for dynamic subtitleText as well as additional appA11yTitle attrs

* moved another method out of html

* Added window id return to single action popout and used the window id to close and abort the popout

* removed duplicate activatedroute

* added a doNotSaveUrl true to 2fa options, so the previousUrl can remain as the fido2 url

* Added a div to restrict the use browser link ot the buttom left

* reverted view change which is handled by the view pr

* Updated locales text and removed unused variable

* Fixed issue where new cipher is not created for non discoverable keys

* switched from using svg for the logo to CL

* removed svg files

* default to browser implmentation if user is logged out of the browser exetension

* removed passkeys knowledge from login, 2fa

* Added fido2 use browser link component and a state service to reduce passkeys knowledge on the lock component

* removed function and removed unnecessary comment

* reverted to former

* [PM-4148] Added descriptive error messages (#6475)

* Added descriptive error messages

* Added descriptive error messages

* replaced fido2 state service with higher order inject functions

* removed null check for tab

* refactor fido2 cipher row component

* added a static abort function to the browser interface service

* removed width from content

* uncommented code

* removed sessionId from query params and redudant styles

* Put back removed sessionId

* Added fallbackRequested parameter to abortPopout and added comments to the standalone function

* minor styling update to fix padding and color on selected ciphers

* update padding again to address vertical pushdown of cipher selection

---------

Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: jng <jng@bitwarden.com>
This commit is contained in:
SmithThe4th
2023-10-10 16:34:54 -04:00
committed by GitHub
parent 94e5117c32
commit 68da3d9efd
28 changed files with 1105 additions and 311 deletions

View File

@@ -2419,6 +2419,21 @@
"message": "Toggle collapse",
"description": "Toggling an expand/collapse state."
},
"aliasDomain": {
"message": "Alias domain"
},
"passwordRepromptDisabledAutofillOnPageLoad": {
"message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.",
"description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load."
},
"autofillOnPageLoadSetToDefault": {
"message": "Auto-fill on page load set to use default setting.",
"description": "Toast message for informing the user that auto-fill on page load has been set to the default setting."
},
"turnOffMasterPasswordPromptToEditField": {
"message": "Turn off master password re-prompt to edit this field",
"description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item."
},
"loginPasskey": {
"message": "This login uses a passkey"
},
@@ -2434,19 +2449,58 @@
"passkeyNotCopiedAlert": {
"message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?"
},
"aliasDomain": {
"message": "Alias domain"
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
"message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password."
},
"passwordRepromptDisabledAutofillOnPageLoad": {
"message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.",
"description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load."
"logInWithPasskey": {
"message": "Log in with passkey?"
},
"autofillOnPageLoadSetToDefault": {
"message": "Auto-fill on page load set to use default setting.",
"description": "Toast message for informing the user that auto-fill on page load has been set to the default setting."
"savePasskeyInBitwarden": {
"message": "Save passkey in Bitwarden?"
},
"turnOffMasterPasswordPromptToEditField": {
"message": "Turn off master password re-prompt to edit this field",
"description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item."
"passkeyAlreadyExists": {
"message": "A passkey already exists for this application."
},
"noPasskeysFoundForThisApplication": {
"message": "No passkeys found for this application."
},
"noMatchingPasskeyLogin": {
"message": "You do not have a matching login for this site."
},
"confirm": {
"message": "Confirm"
},
"savePasskey": {
"message": "Save passkey"
},
"savePasskeyNewLogin": {
"message": "Save passkey as new login"
},
"choosePasskey": {
"message": "Choose a login to save this passkey to"
},
"fido2Item": {
"message": "Fido2 Item"
},
"overwritePasskey": {
"message": "Overwrite passkey?"
},
"overwritePasskeyAlert": {
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
},
"featureNotSupported": {
"message": "Feature not yet supported"
},
"searchLogins": {
"message": "Search all logins"
},
"yourPasskeyIsLocked": {
"message": "Authentication required to use passkey. Verify your identity to continue."
},
"loginToSavePasskey": {
"message": "Log in to use passkeys in Bitwarden"
},
"useBrowserName": {
"message": "Use browser"
}
}

View File

@@ -21,11 +21,6 @@ export const fido2AuthGuard: CanActivateFn = async (
const authStatus = await authService.getAuthStatus();
if (authStatus === AuthenticationStatus.LoggedOut) {
routerService.setPreviousUrl(state.url);
return router.createUrlTree(["/home"], { queryParams: route.queryParams });
}
if (authStatus === AuthenticationStatus.Locked) {
routerService.setPreviousUrl(state.url);
return router.createUrlTree(["/lock"], { queryParams: route.queryParams });

View File

@@ -11,81 +11,91 @@
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content">
<div
class="box-content-row box-content-row-flex"
appBoxRow
*ngIf="pinEnabled || masterPasswordEnabled"
>
<div class="row-main" *ngIf="pinEnabled">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
type="{{ showPassword ? 'text' : 'password' }}"
name="PIN"
class="monospaced"
[(ngModel)]="pin"
required
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="masterPasswordEnabled && !pinEnabled">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
aria-describedby="masterPasswordHelp"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
aria-hidden="true"
></i>
</button>
<ng-container *ngIf="fido2PopoutSessionData$ | async as fido2Data">
<div class="box">
<div class="box-content">
<div
class="box-content-row box-content-row-flex"
appBoxRow
*ngIf="pinEnabled || masterPasswordEnabled"
>
<div class="row-main" *ngIf="pinEnabled">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
type="{{ showPassword ? 'text' : 'password' }}"
name="PIN"
class="monospaced"
[(ngModel)]="pin"
required
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="masterPasswordEnabled && !pinEnabled">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
aria-describedby="masterPasswordHelp"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
aria-hidden="true"
></i>
</button>
</div>
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
<p>
{{
fido2Data.isFido2Session
? ("yourPasskeyIsLocked" | i18n)
: ("yourVaultIsLocked" | i18n)
}}
</p>
{{ "loggedInAsOn" | i18n : email : webVaultHostname }}
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
<p>{{ "yourVaultIsLocked" | i18n }}</p>
{{ "loggedInAsOn" | i18n : email : webVaultHostname }}
<div class="box" *ngIf="biometricLock">
<div class="box-footer no-pad">
<button
type="button"
class="btn primary block"
(click)="unlockBiometric()"
appStopClick
[disabled]="pendingBiometric"
>
{{ "unlockWithBiometrics" | i18n }}
</button>
</div>
</div>
</div>
<div class="box" *ngIf="biometricLock">
<div class="box-footer no-pad">
<button
type="button"
class="btn primary block"
(click)="unlockBiometric()"
appStopClick
[disabled]="pendingBiometric"
>
{{ "unlockWithBiometrics" | i18n }}
</button>
</div>
</div>
<p class="text-center">
<button type="button" appStopClick (click)="logOut()">{{ "logOut" | i18n }}</button>
</p>
<app-private-mode-warning></app-private-mode-warning>
<app-callout *ngIf="biometricError" type="error">{{ biometricError }}</app-callout>
<p class="text-center text-muted" *ngIf="pendingBiometric">
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
</p>
<p class="text-center" *ngIf="!fido2Data.isFido2Session">
<button type="button" appStopClick (click)="logOut()">{{ "logOut" | i18n }}</button>
</p>
<app-private-mode-warning></app-private-mode-warning>
<app-callout *ngIf="biometricError" type="error">{{ biometricError }}</app-callout>
<p class="text-center text-muted" *ngIf="pendingBiometric">
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
</p>
<app-fido2-use-browser-link></app-fido2-use-browser-link>
</ng-container>
</main>
</form>

View File

@@ -23,6 +23,7 @@ import { DialogService } from "@bitwarden/components";
import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors";
import { BrowserRouterService } from "../../platform/popup/services/browser-router.service";
import { fido2PopoutSessionData$ } from "../../vault/fido2/browser-fido2-user-interface.service";
@Component({
selector: "app-lock",
@@ -33,6 +34,7 @@ export class LockComponent extends BaseLockComponent {
biometricError: string;
pendingBiometric = false;
fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor(
router: Router,

View File

@@ -560,8 +560,9 @@ export default class MainBackground {
this.browserPopoutWindowService = new BrowserPopoutWindowService();
this.popupUtilsService = new PopupUtilsService(this.isPrivateMode);
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.popupUtilsService);
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(
this.browserPopoutWindowService
);
this.fido2AuthenticatorService = new Fido2AuthenticatorService(
this.cipherService,
this.fido2UserInterfaceService,
@@ -571,6 +572,7 @@ export default class MainBackground {
this.fido2ClientService = new Fido2ClientService(
this.fido2AuthenticatorService,
this.configService,
this.authService,
this.logService
);

View File

@@ -258,11 +258,11 @@ export default class RuntimeBackground {
return await this.main.fido2ClientService.isFido2FeatureEnabled();
case "fido2RegisterCredentialRequest":
return await this.abortManager.runWithAbortController(msg.requestId, (abortController) =>
this.main.fido2ClientService.createCredential(msg.data, abortController)
this.main.fido2ClientService.createCredential(msg.data, sender.tab, abortController)
);
case "fido2GetCredentialRequest":
return await this.abortManager.runWithAbortController(msg.requestId, (abortController) =>
this.main.fido2ClientService.assertCredential(msg.data, abortController)
this.main.fido2ClientService.assertCredential(msg.data, sender.tab, abortController)
);
}
}

View File

@@ -10,6 +10,15 @@ interface BrowserPopoutWindowService {
}
): Promise<void>;
closePasswordRepromptPrompt(): Promise<void>;
openFido2Popout(
senderWindowId: number,
promptData: {
sessionId: string;
senderTabId: number;
fallbackSupported: boolean;
}
): Promise<number>;
closeFido2Popout(): Promise<void>;
}
export { BrowserPopoutWindowService };

View File

@@ -49,29 +49,65 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface {
await this.closeSingleActionPopout("passwordReprompt");
}
async openFido2Popout(
senderWindowId: number,
{
sessionId,
senderTabId,
fallbackSupported,
}: {
sessionId: string;
senderTabId: number;
fallbackSupported: boolean;
}
): Promise<number> {
await this.closeFido2Popout();
const promptWindowPath =
"popup/index.html#/fido2" +
"?uilocation=popout" +
`&sessionId=${sessionId}` +
`&fallbackSupported=${fallbackSupported}` +
`&senderTabId=${senderTabId}`;
return await this.openSingleActionPopout(senderWindowId, promptWindowPath, "fido2Popout", {
width: 200,
height: 500,
});
}
async closeFido2Popout(): Promise<void> {
await this.closeSingleActionPopout("fido2Popout");
}
private async openSingleActionPopout(
senderWindowId: number,
popupWindowURL: string,
singleActionPopoutKey: string
) {
singleActionPopoutKey: string,
options: chrome.windows.CreateData = {}
): Promise<number> {
const senderWindow = senderWindowId && (await BrowserApi.getWindow(senderWindowId));
const url = chrome.extension.getURL(popupWindowURL);
const offsetRight = 15;
const offsetTop = 90;
const popupWidth = this.defaultPopoutWindowOptions.width;
/// Use overrides in `options` if provided, otherwise use default
const popupWidth = options?.width || this.defaultPopoutWindowOptions.width;
const windowOptions = senderWindow
? {
...this.defaultPopoutWindowOptions,
url,
left: senderWindow.left + senderWindow.width - popupWidth - offsetRight,
top: senderWindow.top + offsetTop,
...options,
url,
}
: { ...this.defaultPopoutWindowOptions, url };
: { ...this.defaultPopoutWindowOptions, url, ...options };
const popupWindow = await BrowserApi.createWindow(windowOptions);
await this.closeSingleActionPopout(singleActionPopoutKey);
this.singleActionPopoutTabIds[singleActionPopoutKey] = popupWindow?.tabs[0].id;
return popupWindow.id;
}
private async closeSingleActionPopout(popoutKey: string) {

View File

@@ -115,7 +115,7 @@ const routes: Routes = [
path: "2fa-options",
component: TwoFactorOptionsComponent,
canActivate: [UnauthGuard],
data: { state: "2fa-options" },
data: { state: "2fa-options", doNotSaveUrl: true },
},
{
path: "login-initiated",

View File

@@ -39,6 +39,8 @@ import { SendTypeComponent } from "../tools/popup/send/send-type.component";
import { ExportComponent } from "../tools/popup/settings/export.component";
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component";
import { Fido2UseBrowserLinkComponent } from "../vault/popup/components/fido2/fido2-use-browser-link.component";
import { Fido2Component } from "../vault/popup/components/fido2/fido2.component";
import { PasswordRepromptComponent } from "../vault/popup/components/password-reprompt.component";
import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component";
@@ -113,6 +115,8 @@ import "../platform/popup/locales";
EnvironmentComponent,
ExcludedDomainsComponent,
ExportComponent,
Fido2CipherRowComponent,
Fido2UseBrowserLinkComponent,
FolderAddEditComponent,
FoldersComponent,
VaultFilterComponent,

View File

@@ -621,29 +621,6 @@ main {
}
}
app-fido2 {
.auth-wrapper {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 25px;
.btn {
margin-top: 25px;
}
}
.box.list {
overflow-y: auto;
}
}
.login-with-device {
.fingerprint-phrase-header {
padding-top: 1rem;

View File

@@ -153,6 +153,14 @@ body.body-full {
margin: 15px 0 15px 0;
}
.useBrowserlink {
padding: 0 10px 5px 10px;
position: fixed;
bottom: 10px;
left: 0;
right: 0;
}
app-options {
.box {
margin: 10px 0;
@@ -175,3 +183,169 @@ app-vault-attachments {
}
}
}
app-fido2 {
.auth-wrapper {
display: flex;
flex-direction: column;
padding: 12px 24px 12px 24px;
.auth-header {
display: flex;
justify-content: space-between;
align-items: center;
.left {
padding-right: 10px;
.logo {
display: inline-flex;
align-items: center;
i.bwi {
font-size: 35px;
margin-right: 3px;
@include themify($themes) {
color: themed("primaryColor");
}
}
span {
font-size: 45px;
font-weight: 300;
margin-top: -3px;
@include themify($themes) {
color: themed("primaryColor");
}
}
}
}
.search {
padding: 7px 10px;
width: 100%;
text-align: left;
position: relative;
display: flex;
.bwi {
position: absolute;
top: 15px;
left: 20px;
@include themify($themes) {
color: themed("labelColor");
}
}
input {
width: 100%;
margin: 0;
border: none;
padding: 5px 10px 5px 30px;
border-radius: $border-radius;
&:focus {
border-radius: $border-radius;
outline: none;
}
&[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
background-repeat: no-repeat;
mask-image: none;
-webkit-mask-image: none;
}
}
}
}
.auth-flow {
display: flex;
align-items: flex-start;
flex-direction: column;
margin-top: 32px;
margin-bottom: 32px;
.subtitle {
font-family: Open Sans;
font-size: 24px;
font-style: normal;
font-weight: 600;
line-height: 32px;
}
.box.list {
overflow-y: auto;
}
.box-content {
max-height: 140px;
}
@media screen and (min-height: 501px) and (max-height: 600px) {
.box-content {
max-height: 200px;
}
}
@media screen and (min-height: 601px) {
.box-content {
max-height: 260px;
}
}
.box-content-row {
display: flex;
justify-content: center;
align-items: center;
margin: 0px;
padding: 0px;
margin-bottom: 12px;
button {
min-height: 44px;
}
.row-main {
border-radius: 6px;
padding: 5px 0px 5px 12px;
&:focus {
@include themify($themes) {
border: 2px solid themed("headerInputBackgroundFocusColor");
}
}
&.row-selected {
@include themify($themes) {
outline: none;
border-left: 5px solid themed("primaryColor");
padding: 3px 0px 3px 7px;
background-color: themed("headerBackgroundHoverColor");
color: themed("headerColor");
}
}
}
.row-main-content {
display: flex;
flex-direction: column;
justify-content: center;
.detail {
min-height: 15px;
display: block;
}
}
}
.btn {
width: 100%;
font-size: 16px;
font-weight: 600;
}
}
}
}

View File

@@ -1,9 +1,13 @@
import { inject } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import {
BehaviorSubject,
EmptyError,
filter,
firstValueFrom,
fromEvent,
fromEventPattern,
map,
merge,
Observable,
Subject,
@@ -11,7 +15,6 @@ import {
take,
takeUntil,
throwError,
fromEventPattern,
} from "rxjs";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -24,10 +27,26 @@ import {
} from "@bitwarden/common/vault/abstractions/fido2/fido2-user-interface.service.abstraction";
import { BrowserApi } from "../../platform/browser/browser-api";
import { Popout, PopupUtilsService } from "../../popup/services/popup-utils.service";
import { BrowserPopoutWindowService } from "../../platform/popup/abstractions/browser-popout-window.service";
const BrowserFido2MessageName = "BrowserFido2UserInterfaceServiceMessage";
/**
* Function to retrieve FIDO2 session data from query parameters.
* Expected to be used within components tied to routes with these query parameters.
*/
export function fido2PopoutSessionData$() {
const route = inject(ActivatedRoute);
return route.queryParams.pipe(
map((queryParams) => ({
isFido2Session: queryParams.sessionId != null,
sessionId: queryParams.sessionId as string,
fallbackSupported: queryParams.fallbackSupported as boolean,
}))
);
}
export class SessionClosedError extends Error {
constructor() {
super("Fido2UserInterfaceSession was closed");
@@ -94,15 +113,17 @@ export type BrowserFido2Message = { sessionId: string } & (
* The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it.
*/
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
constructor(private popupUtilsService: PopupUtilsService) {}
constructor(private browserPopoutWindowService: BrowserPopoutWindowService) {}
async newSession(
fallbackSupported: boolean,
tab: chrome.tabs.Tab,
abortController?: AbortController
): Promise<Fido2UserInterfaceSession> {
return await BrowserFido2UserInterfaceSession.create(
this.popupUtilsService,
this.browserPopoutWindowService,
fallbackSupported,
tab,
abortController
);
}
@@ -110,13 +131,15 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession {
static async create(
popupUtilsService: PopupUtilsService,
browserPopoutWindowService: BrowserPopoutWindowService,
fallbackSupported: boolean,
tab: chrome.tabs.Tab,
abortController?: AbortController
): Promise<BrowserFido2UserInterfaceSession> {
return new BrowserFido2UserInterfaceSession(
popupUtilsService,
browserPopoutWindowService,
fallbackSupported,
tab,
abortController
);
}
@@ -125,19 +148,26 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
BrowserApi.sendMessage(BrowserFido2MessageName, msg);
}
static abortPopout(sessionId: string, fallbackRequested = false) {
this.sendMessage({
sessionId: sessionId,
type: "AbortResponse",
fallbackRequested: fallbackRequested,
});
}
private closed = false;
private messages$ = (BrowserApi.messageListener$() as Observable<BrowserFido2Message>).pipe(
filter((msg) => msg.sessionId === this.sessionId)
);
private windowClosed$: Observable<number>;
private tabClosed$: Observable<number>;
private connected$ = new BehaviorSubject(false);
private windowClosed$: Observable<number>;
private destroy$ = new Subject<void>();
private popout?: Popout;
private constructor(
private readonly popupUtilsService: PopupUtilsService,
private readonly browserPopoutWindowService: BrowserPopoutWindowService,
private readonly fallbackSupported: boolean,
private readonly tab: chrome.tabs.Tab,
readonly abortController = new AbortController(),
readonly sessionId = Utils.newGuid()
) {
@@ -181,11 +211,6 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
(handler: any) => chrome.windows.onRemoved.removeListener(handler)
);
this.tabClosed$ = fromEventPattern(
(handler: any) => chrome.tabs.onRemoved.addListener(handler),
(handler: any) => chrome.tabs.onRemoved.removeListener(handler)
);
BrowserFido2UserInterfaceSession.sendMessage({
type: "NewSessionCreatedRequest",
sessionId,
@@ -258,7 +283,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
}
async close() {
this.popupUtilsService.closePopOut(this.popout);
await this.browserPopoutWindowService.closeFido2Popout();
this.closed = true;
this.destroy$.next();
this.destroy$.complete();
@@ -299,7 +324,6 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
throw new Error("Cannot re-open closed session");
}
// create promise first to avoid race condition where the popout opens before we start listening
const connectPromise = firstValueFrom(
merge(
this.connected$.pipe(filter((connected) => connected === true)),
@@ -309,41 +333,24 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
)
);
this.popout = await this.generatePopOut();
const popoutId = await this.browserPopoutWindowService.openFido2Popout(this.tab.windowId, {
sessionId: this.sessionId,
senderTabId: this.tab.id,
fallbackSupported: this.fallbackSupported,
});
if (this.popout.type === "window") {
const popoutWindow = this.popout;
this.windowClosed$
.pipe(
filter((windowId) => popoutWindow.window.id === windowId),
takeUntil(this.destroy$)
)
.subscribe(() => {
this.close();
this.abort();
});
} else if (this.popout.type === "tab") {
const popoutTab = this.popout;
this.tabClosed$
.pipe(
filter((tabId) => popoutTab.tab.id === tabId),
takeUntil(this.destroy$)
)
.subscribe(() => {
this.close();
this.abort();
});
}
this.windowClosed$
.pipe(
filter((windowId) => {
return popoutId === windowId;
}),
takeUntil(this.destroy$)
)
.subscribe(() => {
this.close();
this.abort();
});
await connectPromise;
}
private async generatePopOut() {
const queryParams = new URLSearchParams({ sessionId: this.sessionId });
return this.popupUtilsService.popOut(
null,
`popup/index.html?uilocation=popout#/fido2?${queryParams.toString()}`,
{ center: true }
);
}
}

View File

@@ -0,0 +1,27 @@
<div
role="group"
appA11yTitle="{{ cipher.name }} {{ cipher.subTitle }}"
class="virtual-scroll-item"
[ngClass]="{ 'override-last': !last }"
>
<div class="box-content-row box-content-row-flex">
<button
type="button"
(click)="selectCipher(cipher)"
tabindex="0"
appStopClick
title="{{ title }} - {{ cipher.name }}"
[ngClass]="{ 'row-main': true, 'row-selected': isSelected && !isSearching }"
>
<app-vault-icon [cipher]="cipher"></app-vault-icon>
<div class="row-main-content">
<span class="text">
<span class="truncate-box">
<span class="truncate">{{ cipher.name }}</span>
</span>
</span>
<span class="detail" *ngIf="cipher.subTitle">{{ cipher.subTitle }}</span>
</div>
</button>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Component({
selector: "app-fido2-cipher-row",
templateUrl: "fido2-cipher-row.component.html",
})
export class Fido2CipherRowComponent {
@Output() onSelected = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@Input() last: boolean;
@Input() title: string;
@Input() isSearching: boolean;
@Input() isSelected: boolean;
selectCipher(c: CipherView) {
this.onSelected.emit(c);
}
}

View File

@@ -0,0 +1,5 @@
<div class="useBrowserlink" *ngIf="(fido2PopoutSessionData$ | async).fallbackSupported">
<a [routerLink]="[]" (click)="abort()">
{{ "useBrowserName" | i18n }}
</a>
</div>

View File

@@ -0,0 +1,21 @@
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
import {
BrowserFido2UserInterfaceSession,
fido2PopoutSessionData$,
} from "../../../fido2/browser-fido2-user-interface.service";
@Component({
selector: "app-fido2-use-browser-link",
templateUrl: "fido2-use-browser-link.component.html",
})
export class Fido2UseBrowserLinkComponent {
fido2PopoutSessionData$ = fido2PopoutSessionData$();
async abort() {
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId, true);
return;
}
}

View File

@@ -1,49 +1,139 @@
<!-- TODO: Rewrite and refactor this component when implementing the new design -->
<ng-container *ngIf="data$ | async as data">
<div class="auth-wrapper">
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
<ng-container *ngIf="data.showUnsupportedVerification">
Verification required by the initiating site. This feature is not yet implemented for accounts
without master password.
</ng-container>
<ng-container *ngIf="!data.showUnsupportedVerification">
<div class="auth-header">
<div class="left">
<ng-container *ngIf="data.message.type != 'PickCredentialRequest'">
<div class="logo">
<i class="bwi bwi-shield"></i>
</div>
</ng-container>
<ng-container *ngIf="data.message.type == 'PickCredentialRequest'">
<div class="logo">
<i class="bwi bwi-shield"></i><span><strong>bit</strong>warden</span>
</div>
</ng-container>
</div>
<ng-container *ngIf="data.message.type == 'ConfirmNewCredentialRequest'">
<div class="search">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ 'searchVault' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search(200)"
autocomplete="off"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</ng-container>
</div>
<ng-container>
<ng-container
*ngIf="
data.message.type == 'PickCredentialRequest' ||
data.message.type == 'ConfirmNewCredentialRequest'
data.message.type === 'PickCredentialRequest' ||
data.message.type === 'ConfirmNewCredentialRequest'
"
>
A site is asking for authentication, please choose one of the following credentials to use:
<div class="box list">
<div class="box-content">
<app-cipher-row
*ngFor="let cipher of ciphers"
[cipher]="cipher"
(onSelected)="pick(cipher)"
></app-cipher-row>
</div>
<div class="auth-flow">
<p class="subtitle" appA11yTitle="{{ subtitleText | i18n }}">
{{ subtitleText | i18n }}
</p>
<!-- Display when ciphers exist -->
<ng-container *ngIf="displayedCiphers.length > 0">
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
[isSearching]="searchPending"
title="{{ 'fido2Item' | i18n }}"
(onSelected)="selectedPasskey($event)"
[isSelected]="cipher === cipherItem"
></app-fido2-cipher-row>
</div>
</div>
<div class="box">
<button
type="submit"
(click)="submit()"
class="btn primary block"
appA11yTitle="{{ credentialText | i18n }}"
>
<span [hidden]="loading">
{{ credentialText | i18n }}
</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!loading"
aria-hidden="true"
></i>
</button>
</div>
</ng-container>
<ng-container *ngIf="!displayedCiphers.length">
<div class="box">
<button
type="submit"
(click)="saveNewLogin()"
class="btn primary block"
appA11yTitle="{{ 'savePasskeyNewLogin' | i18n }}"
>
<span [hidden]="loading">
{{ "savePasskeyNewLogin" | i18n }}
</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!loading"
aria-hidden="true"
></i>
</button>
</div>
</ng-container>
</div>
</ng-container>
<ng-container *ngIf="data.message.type == 'InformExcludedCredentialRequest'">
A passkey already exists in Bitwarden for this account
<div class="box list">
<div class="box-content">
<app-cipher-row *ngFor="let cipher of ciphers" [cipher]="cipher"></app-cipher-row>
<div class="auth-flow">
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'fido2Item' | i18n }}"
(onSelected)="selectedPasskey($event)"
[isSelected]="cipher === cipherItem"
></app-fido2-cipher-row>
</div>
</div>
<button type="button" class="btn primary block" (click)="viewPasskey()">
<span [hidden]="loading">{{ "viewItem" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</div>
</ng-container>
<ng-container *ngIf="data.message.type == 'InformCredentialNotFoundRequest'">
You do not have a matching login for this site.
<div class="auth-flow">
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
</div>
<button type="button" class="btn primary block" (click)="abort(false)">
<span [hidden]="loading">{{ "close" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</ng-container>
</ng-container>
<button
*ngIf="data.fallbackSupported"
type="button"
class="btn btn-outline-secondary"
(click)="abort(true)"
>
Use browser built-in
</button>
<button type="button" class="btn btn-outline-secondary" (click)="abort(false)">Abort</button>
<div class="useBrowserlink">
<a [routerLink]="[]" *ngIf="data.fallbackSupported" (click)="abort(true)">
{{ "useBrowserName" | i18n }}
</a>
</div>
</div>
</ng-container>

View File

@@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ActivatedRoute, Router } from "@angular/router";
import {
BehaviorSubject,
combineLatest,
@@ -12,10 +12,23 @@ import {
takeUntil,
} from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { SecureNoteType } from "@bitwarden/common/enums";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.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";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import {
@@ -25,7 +38,6 @@ import {
interface ViewData {
message: BrowserFido2Message;
showUnsupportedVerification: boolean;
fallbackSupported: boolean;
}
@@ -36,89 +48,177 @@ interface ViewData {
})
export class Fido2Component implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
private hasSearched = false;
private searchTimeout: any = null;
private hasLoadedAllCiphers = false;
protected cipher: CipherView;
protected searchTypeSearch = false;
protected searchPending = false;
protected searchText: string;
protected url: string;
protected hostname: string;
protected data$: Observable<ViewData>;
protected sessionId?: string;
protected senderTabId?: string;
protected ciphers?: CipherView[] = [];
protected displayedCiphers?: CipherView[] = [];
protected loading = false;
protected subtitleText: string;
protected credentialText: string;
private message$ = new BehaviorSubject<BrowserFido2Message>(null);
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService
private passwordRepromptService: PasswordRepromptService,
private platformUtilsService: PlatformUtilsService,
private settingsService: SettingsService,
private searchService: SearchService,
private logService: LogService,
private dialogService: DialogService
) {}
ngOnInit(): void {
const sessionId$ = this.activatedRoute.queryParamMap.pipe(
ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
const queryParams$ = this.activatedRoute.queryParamMap.pipe(
take(1),
map((queryParamMap) => queryParamMap.get("sessionId"))
map((queryParamMap) => ({
sessionId: queryParamMap.get("sessionId"),
senderTabId: queryParamMap.get("senderTabId"),
}))
);
combineLatest([sessionId$, BrowserApi.messageListener$() as Observable<BrowserFido2Message>])
.pipe(takeUntil(this.destroy$))
.subscribe(([sessionId, message]) => {
this.sessionId = sessionId;
if (message.type === "NewSessionCreatedRequest" && message.sessionId !== sessionId) {
return this.abort(false);
}
combineLatest([queryParams$, BrowserApi.messageListener$() as Observable<BrowserFido2Message>])
.pipe(
concatMap(async ([queryParams, message]) => {
this.sessionId = queryParams.sessionId;
this.senderTabId = queryParams.senderTabId;
if (message.sessionId !== sessionId) {
return;
}
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
if (
message.type === "NewSessionCreatedRequest" &&
message.sessionId !== queryParams.sessionId
) {
this.abort(false);
return;
}
if (message.type === "AbortRequest") {
return this.abort(false);
}
// Ignore messages that don't belong to the current session.
if (message.sessionId !== queryParams.sessionId) {
return;
}
if (message.type === "AbortRequest") {
this.abort(false);
return;
}
// Show dialog if user account does not have master password
if (!(await this.passwordRepromptService.enabled())) {
await this.dialogService.openSimpleDialog({
title: { key: "featureNotSupported" },
content: { key: "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "info",
});
this.abort(true);
return;
}
return message;
}),
filter((message) => !!message),
takeUntil(this.destroy$)
)
.subscribe((message) => {
this.message$.next(message);
});
this.data$ = this.message$.pipe(
filter((message) => message != undefined),
concatMap(async (message) => {
if (message.type === "ConfirmNewCredentialRequest") {
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted
);
} else if (message.type === "PickCredentialRequest") {
this.ciphers = await Promise.all(
message.cipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId);
return cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher));
})
);
} else if (message.type === "InformExcludedCredentialRequest") {
this.ciphers = await Promise.all(
message.existingCipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId);
return cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher));
})
);
switch (message.type) {
case "ConfirmNewCredentialRequest": {
const activeTabs = await BrowserApi.getActiveTabs();
this.url = activeTabs[0].url;
const equivalentDomains = this.settingsService.getEquivalentDomains(this.url);
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted
);
this.displayedCiphers = this.ciphers.filter((cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains)
);
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
break;
}
case "PickCredentialRequest": {
this.ciphers = await Promise.all(
message.cipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
})
);
this.displayedCiphers = [...this.ciphers];
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
break;
}
case "InformExcludedCredentialRequest": {
this.ciphers = await Promise.all(
message.existingCipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
})
);
this.displayedCiphers = [...this.ciphers];
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
break;
}
}
this.subtitleText =
this.displayedCiphers.length > 0
? this.getCredentialSubTitleText(message.type)
: "noMatchingPasskeyLogin";
this.credentialText = this.getCredentialButtonText(message.type);
return {
message,
showUnsupportedVerification:
"userVerification" in message &&
message.userVerification &&
!(await this.passwordRepromptService.enabled()),
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
};
}),
takeUntil(this.destroy$)
);
sessionId$.pipe(takeUntil(this.destroy$)).subscribe((sessionId) => {
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
this.send({
sessionId: sessionId,
sessionId: queryParams.sessionId,
type: "ConnectResponse",
});
});
}
async pick(cipher: CipherView) {
async submit() {
const data = this.message$.value;
if (data?.type === "PickCredentialRequest") {
let userVerified = false;
@@ -128,19 +228,32 @@ export class Fido2Component implements OnInit, OnDestroy {
this.send({
sessionId: this.sessionId,
cipherId: cipher.id,
cipherId: this.cipher.id,
type: "PickCredentialResponse",
userVerified,
});
} else if (data?.type === "ConfirmNewCredentialRequest") {
let userVerified = false;
if (this.cipher.login.fido2Credentials.length > 0) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "overwritePasskey" },
content: { key: "overwritePasskeyAlert" },
type: "info",
});
if (!confirmed) {
return false;
}
}
if (data.userVerification) {
userVerified = await this.passwordRepromptService.showPasswordPrompt();
}
this.send({
sessionId: this.sessionId,
cipherId: cipher.id,
cipherId: this.cipher.id,
type: "ConfirmNewCredentialResponse",
userVerified,
});
@@ -149,6 +262,131 @@ export class Fido2Component implements OnInit, OnDestroy {
this.loading = true;
}
async saveNewLogin() {
const data = this.message$.value;
if (data?.type === "ConfirmNewCredentialRequest") {
let userVerified = false;
if (data.userVerification) {
userVerified = await this.passwordRepromptService.showPasswordPrompt();
}
if (!data.userVerification || userVerified) {
await this.createNewCipher();
}
this.send({
sessionId: this.sessionId,
cipherId: this.cipher?.id,
type: "ConfirmNewCredentialResponse",
userVerified,
});
}
this.loading = true;
}
getCredentialSubTitleText(messageType: string): string {
return messageType == "ConfirmNewCredentialRequest" ? "choosePasskey" : "logInWithPasskey";
}
getCredentialButtonText(messageType: string): string {
return messageType == "ConfirmNewCredentialRequest" ? "savePasskey" : "confirm";
}
selectedPasskey(item: CipherView) {
this.cipher = item;
}
viewPasskey() {
this.router.navigate(["/view-cipher"], {
queryParams: {
cipherId: this.cipher.id,
uilocation: "popout",
senderTabId: this.senderTabId,
sessionId: this.sessionId,
},
});
}
addCipher() {
this.router.navigate(["/add-cipher"], {
queryParams: {
name: Utils.getHostname(this.url),
uri: this.url,
uilocation: "popout",
senderTabId: this.senderTabId,
sessionId: this.sessionId,
},
});
}
buildCipher() {
this.cipher = new CipherView();
this.cipher.name = Utils.getHostname(this.url);
this.cipher.type = CipherType.Login;
this.cipher.login = new LoginView();
this.cipher.login.uris = [new LoginUriView()];
this.cipher.login.uris[0].uri = this.url;
this.cipher.card = new CardView();
this.cipher.identity = new IdentityView();
this.cipher.secureNote = new SecureNoteView();
this.cipher.secureNote.type = SecureNoteType.Generic;
this.cipher.reprompt = CipherRepromptType.None;
}
async createNewCipher() {
this.buildCipher();
const cipher = await this.cipherService.encrypt(this.cipher);
try {
await this.cipherService.createWithServer(cipher);
this.cipher.id = cipher.id;
} catch (e) {
this.logService.error(e);
}
}
async loadLoginCiphers() {
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted
);
if (!this.hasLoadedAllCiphers) {
this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText);
}
await this.search(null);
}
async search(timeout: number = null) {
this.searchPending = false;
if (this.searchTimeout != null) {
clearTimeout(this.searchTimeout);
}
if (timeout == null) {
this.hasSearched = this.searchService.isSearchable(this.searchText);
this.displayedCiphers = await this.searchService.searchCiphers(
this.searchText,
null,
this.ciphers
);
return;
}
this.searchPending = true;
this.searchTimeout = setTimeout(async () => {
this.hasSearched = this.searchService.isSearchable(this.searchText);
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
await this.loadLoginCiphers();
} else {
this.displayedCiphers = await this.searchService.searchCiphers(
this.searchText,
null,
this.ciphers
);
}
this.searchPending = false;
this.selectedPasskey(this.displayedCiphers[0]);
}, timeout);
}
abort(fallback: boolean) {
this.unload(fallback);
window.close();

View File

@@ -1,7 +1,8 @@
import { Location } from "@angular/common";
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { firstValueFrom } from "rxjs";
import { first, takeUntil } from "rxjs/operators";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
@@ -24,6 +25,10 @@ import { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
import {
BrowserFido2UserInterfaceSession,
fido2PopoutSessionData$,
} from "../../../fido2/browser-fido2-user-interface.service";
@Component({
selector: "app-vault-add-edit",
@@ -35,6 +40,11 @@ export class AddEditComponent extends BaseAddEditComponent {
showAttachments = true;
openAttachmentsInPopup: boolean;
showAutoFillOnPageLoadOptions: boolean;
inPopout = false;
senderTabId?: number;
uilocation?: "popout" | "popup" | "sidebar" | "tab";
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor(
cipherService: CipherService,
@@ -79,6 +89,13 @@ export class AddEditComponent extends BaseAddEditComponent {
async ngOnInit() {
await super.ngOnInit();
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((value) => {
this.senderTabId = parseInt(value?.senderTabId, 10) || undefined;
this.uilocation = value?.uilocation;
});
this.inPopout = this.uilocation === "popout" || this.popupUtilsService.inPopout(window);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) {
@@ -156,6 +173,12 @@ export class AddEditComponent extends BaseAddEditComponent {
return false;
}
// Would be refactored after rework is done on the windows popout service
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (this.inPopout && sessionData.isFido2Session) {
return;
}
if (this.popupUtilsService.inTab(window)) {
this.popupUtilsService.disableCloseTabWarning();
this.messagingService.send("closeTab", { delay: 1000 });
@@ -191,17 +214,37 @@ export class AddEditComponent extends BaseAddEditComponent {
}
}
cancel() {
async cancel() {
super.cancel();
// Would be refactored after rework is done on the windows popout service
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (this.inPopout && sessionData.isFido2Session) {
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
return;
}
if (this.popupUtilsService.inTab(window)) {
this.messagingService.send("closeTab");
return;
}
if (this.inPopout && this.senderTabId) {
this.close();
return;
}
this.location.back();
}
// Used for closing single-action views
close() {
BrowserApi.focusTab(this.senderTabId);
window.close();
return;
}
async generateUsername(): Promise<boolean> {
const confirmed = await super.generateUsername();
if (confirmed) {

View File

@@ -1,7 +1,7 @@
import { Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { Subject, firstValueFrom, takeUntil } from "rxjs";
import { first } from "rxjs/operators";
import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component";
@@ -28,6 +28,10 @@ import { DialogService } from "@bitwarden/components";
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
import {
BrowserFido2UserInterfaceSession,
fido2PopoutSessionData$,
} from "../../../fido2/browser-fido2-user-interface.service";
const BroadcasterSubscriptionId = "ChildViewComponent";
@@ -55,6 +59,7 @@ export class ViewComponent extends BaseViewComponent {
uilocation?: "popout" | "popup" | "sidebar" | "tab";
loadPageDetailsTimeout: number;
inPopout = false;
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
private destroy$ = new Subject<void>();
@@ -299,7 +304,14 @@ export class ViewComponent extends BaseViewComponent {
return false;
}
close() {
async close() {
// Would be refactored after rework is done on the windows popout service
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (this.inPopout && sessionData.isFido2Session) {
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
return;
}
if (this.inPopout && this.senderTabId) {
BrowserApi.focusTab(this.senderTabId);
window.close();