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:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="useBrowserlink" *ngIf="(fido2PopoutSessionData$ | async).fallbackSupported">
|
||||
<a [routerLink]="[]" (click)="abort()">
|
||||
{{ "useBrowserName" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user