mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +00:00
[PM-7956] Update passkey pop-out to new UI (#10796)
* rename existing fido2 components to use v1 designation * use fido2 message type value constants in components * add v2 fido2 components * add search to login UX of fido2 v2 component * add new item button in top nav of fido2 v2 component * get and pass activeUserId to cipher key decription methods * cleanup / PR suggestions
This commit is contained in:
@@ -3477,7 +3477,7 @@
|
|||||||
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
|
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
|
||||||
"message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password."
|
"message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password."
|
||||||
},
|
},
|
||||||
"logInWithPasskey": {
|
"logInWithPasskeyQuestion": {
|
||||||
"message": "Log in with passkey?"
|
"message": "Log in with passkey?"
|
||||||
},
|
},
|
||||||
"passkeyAlreadyExists": {
|
"passkeyAlreadyExists": {
|
||||||
@@ -3489,6 +3489,9 @@
|
|||||||
"noMatchingPasskeyLogin": {
|
"noMatchingPasskeyLogin": {
|
||||||
"message": "You do not have a matching login for this site."
|
"message": "You do not have a matching login for this site."
|
||||||
},
|
},
|
||||||
|
"noMatchingLoginsForSite": {
|
||||||
|
"message": "No matching logins for this site"
|
||||||
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"message": "Confirm"
|
"message": "Confirm"
|
||||||
},
|
},
|
||||||
@@ -3498,9 +3501,12 @@
|
|||||||
"savePasskeyNewLogin": {
|
"savePasskeyNewLogin": {
|
||||||
"message": "Save passkey as new login"
|
"message": "Save passkey as new login"
|
||||||
},
|
},
|
||||||
"choosePasskey": {
|
"chooseCipherForPasskeySave": {
|
||||||
"message": "Choose a login to save this passkey to"
|
"message": "Choose a login to save this passkey to"
|
||||||
},
|
},
|
||||||
|
"chooseCipherForPasskeyAuth": {
|
||||||
|
"message": "Choose a passkey to log in with"
|
||||||
|
},
|
||||||
"passkeyItem": {
|
"passkeyItem": {
|
||||||
"message": "Passkey Item"
|
"message": "Passkey Item"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
|
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<app-fido2-use-browser-link></app-fido2-use-browser-link>
|
<app-fido2-use-browser-link-v1></app-fido2-use-browser-link-v1>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</main>
|
</main>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export enum MessageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The params provided by the page-script are created in an insecure environemnt and
|
* The params provided by the page-script are created in an insecure environment and
|
||||||
* should not be trusted. This type is used to ensure that the content-script does not
|
* should not be trusted. This type is used to ensure that the content-script does not
|
||||||
* trust the `origin` or `sameOriginWithAncestors` params.
|
* trust the `origin` or `sameOriginWithAncestors` params.
|
||||||
*/
|
*/
|
||||||
@@ -38,7 +38,7 @@ export type CredentialCreationResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The params provided by the page-script are created in an insecure environemnt and
|
* The params provided by the page-script are created in an insecure environment and
|
||||||
* should not be trusted. This type is used to ensure that the content-script does not
|
* should not be trusted. This type is used to ensure that the content-script does not
|
||||||
* trust the `origin` or `sameOriginWithAncestors` params.
|
* trust the `origin` or `sameOriginWithAncestors` params.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<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>
|
||||||
|
<ng-container *ngIf="cipher.organizationId">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-collection text-muted"
|
||||||
|
title="{{ 'shared' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
|
||||||
|
<span class="detail" *ngIf="cipher.subTitle">{{ cipher.subTitle }}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
|
||||||
|
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-fido2-cipher-row-v1",
|
||||||
|
templateUrl: "fido2-cipher-row-v1.component.html",
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class Fido2CipherRowV1Component {
|
||||||
|
@Output() onSelected = new EventEmitter<CipherView>();
|
||||||
|
@Input() cipher: CipherView;
|
||||||
|
@Input() last: boolean;
|
||||||
|
@Input() title: string;
|
||||||
|
@Input() isSearching: boolean;
|
||||||
|
@Input() isSelected: boolean;
|
||||||
|
|
||||||
|
protected selectCipher(c: CipherView) {
|
||||||
|
this.onSelected.emit(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a subname for the cipher.
|
||||||
|
* If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId.
|
||||||
|
* @param c Cipher
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected getSubName(c: CipherView): string | null {
|
||||||
|
const fido2Credentials = c.login?.fido2Credentials;
|
||||||
|
|
||||||
|
if (!fido2Credentials || fido2Credentials.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [fido2Credential] = fido2Credentials;
|
||||||
|
|
||||||
|
return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,21 @@
|
|||||||
<div
|
<bit-item>
|
||||||
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
|
<button
|
||||||
type="button"
|
|
||||||
(click)="selectCipher(cipher)"
|
(click)="selectCipher(cipher)"
|
||||||
|
appA11yTitle="{{ title }} - {{ cipher.name }}"
|
||||||
|
bit-item-content
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
appStopClick
|
type="button"
|
||||||
title="{{ title }} - {{ cipher.name }}"
|
|
||||||
[ngClass]="{ 'row-main': true, 'row-selected': isSelected && !isSearching }"
|
|
||||||
>
|
>
|
||||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||||
<div class="row-main-content">
|
<span data-testid="item-name">
|
||||||
<span class="text">
|
{{ cipher.name }}
|
||||||
<span class="truncate-box">
|
|
||||||
<span class="truncate">{{ cipher.name }}</span>
|
|
||||||
<ng-container *ngIf="cipher.organizationId">
|
|
||||||
<i
|
<i
|
||||||
|
*ngIf="cipher.organizationId"
|
||||||
|
[appA11yTitle]="'shared' | i18n"
|
||||||
class="bwi bwi-collection text-muted"
|
class="bwi bwi-collection text-muted"
|
||||||
title="{{ 'shared' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
|
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
|
||||||
<span class="detail" *ngIf="cipher.subTitle">{{ cipher.subTitle }}</span>
|
<span *ngIf="cipher.subTitle" slot="secondary">{{ cipher.subTitle }}</span>
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</bit-item>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,19 +1,40 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
|
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import {
|
||||||
|
BadgeModule,
|
||||||
|
ButtonModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-fido2-cipher-row",
|
selector: "app-fido2-cipher-row",
|
||||||
templateUrl: "fido2-cipher-row.component.html",
|
templateUrl: "fido2-cipher-row.component.html",
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
BadgeModule,
|
||||||
|
ButtonModule,
|
||||||
|
CommonModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
JslibModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
TypographyModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class Fido2CipherRowComponent {
|
export class Fido2CipherRowComponent {
|
||||||
@Output() onSelected = new EventEmitter<CipherView>();
|
@Output() onSelected = new EventEmitter<CipherView>();
|
||||||
@Input() cipher: CipherView;
|
@Input() cipher: CipherView;
|
||||||
@Input() last: boolean;
|
@Input() last: boolean;
|
||||||
@Input() title: string;
|
@Input() title: string;
|
||||||
@Input() isSearching: boolean;
|
|
||||||
@Input() isSelected: boolean;
|
|
||||||
|
|
||||||
protected selectCipher(c: CipherView) {
|
protected selectCipher(c: CipherView) {
|
||||||
this.onSelected.emit(c);
|
this.onSelected.emit(c);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<ng-container *ngIf="(fido2PopoutSessionData$ | async).fallbackSupported">
|
||||||
|
<div class="useBrowserlink">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="toggle()"
|
||||||
|
cdkOverlayOrigin
|
||||||
|
#trigger="cdkOverlayOrigin"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
aria-controls="cdk-overlay-container"
|
||||||
|
>
|
||||||
|
<span class="text-primary">
|
||||||
|
{{ "useDeviceOrHardwareKey" | i18n }}
|
||||||
|
</span>
|
||||||
|
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template
|
||||||
|
cdkConnectedOverlay
|
||||||
|
[cdkConnectedOverlayOrigin]="trigger"
|
||||||
|
[cdkConnectedOverlayOpen]="isOpen"
|
||||||
|
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||||
|
[cdkConnectedOverlayHasBackdrop]="true"
|
||||||
|
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||||
|
(backdropClick)="isOpen = false"
|
||||||
|
(detach)="close()"
|
||||||
|
>
|
||||||
|
<div class="box-content">
|
||||||
|
<div
|
||||||
|
class="fido2-browser-selector-dropdown"
|
||||||
|
[@transformPanel]="'open'"
|
||||||
|
cdkTrapFocus
|
||||||
|
cdkTrapFocusAutoCapture
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort(false)">
|
||||||
|
<span>{{ "justOnce" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
<br />
|
||||||
|
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort()">
|
||||||
|
<span>{{ "alwaysForThisSite" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
*ngIf="showOverlay"
|
||||||
|
class="tw-absolute tw-w-full tw-h-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
|
||||||
|
></div>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||||
|
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
|
import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data";
|
||||||
|
import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-fido2-use-browser-link-v1",
|
||||||
|
templateUrl: "fido2-use-browser-link-v1.component.html",
|
||||||
|
animations: [
|
||||||
|
trigger("transformPanel", [
|
||||||
|
state(
|
||||||
|
"void",
|
||||||
|
style({
|
||||||
|
opacity: 0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
transition(
|
||||||
|
"void => open",
|
||||||
|
animate(
|
||||||
|
"100ms linear",
|
||||||
|
style({
|
||||||
|
opacity: 1,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class Fido2UseBrowserLinkV1Component {
|
||||||
|
showOverlay = false;
|
||||||
|
isOpen = false;
|
||||||
|
overlayPosition: ConnectedPosition[] = [
|
||||||
|
{
|
||||||
|
originX: "start",
|
||||||
|
originY: "bottom",
|
||||||
|
overlayX: "start",
|
||||||
|
overlayY: "top",
|
||||||
|
offsetY: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
protected fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aborts the current FIDO2 session and fallsback to the browser.
|
||||||
|
* @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts.
|
||||||
|
*/
|
||||||
|
protected async abort(excludeDomain = true) {
|
||||||
|
this.close();
|
||||||
|
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||||
|
|
||||||
|
if (!excludeDomain) {
|
||||||
|
this.abortSession(sessionData.sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Show overlay to prevent the user from interacting with the page.
|
||||||
|
this.showOverlay = true;
|
||||||
|
await this.handleDomainExclusion(sessionData.senderUrl);
|
||||||
|
// Give the user a chance to see the toast before closing the popout.
|
||||||
|
await Utils.delay(2000);
|
||||||
|
this.abortSession(sessionData.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excludes the domain from future FIDO2 prompts.
|
||||||
|
* @param uri - The domain uri to exclude from future FIDO2 prompts.
|
||||||
|
*/
|
||||||
|
private async handleDomainExclusion(uri: string) {
|
||||||
|
const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
|
||||||
|
|
||||||
|
const validDomain = Utils.getHostname(uri);
|
||||||
|
const savedDomains: NeverDomains = {
|
||||||
|
...existingDomains,
|
||||||
|
};
|
||||||
|
savedDomains[validDomain] = null;
|
||||||
|
|
||||||
|
await this.domainSettingsService.setNeverDomains(savedDomains);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("domainAddedToExcludedDomains", validDomain),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private abortSession(sessionId: string) {
|
||||||
|
BrowserFido2UserInterfaceSession.abortPopout(sessionId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
import { A11yModule } from "@angular/cdk/a11y";
|
||||||
|
import { ConnectedPosition, CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -15,6 +18,8 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "app-fido2-use-browser-link",
|
selector: "app-fido2-use-browser-link",
|
||||||
templateUrl: "fido2-use-browser-link.component.html",
|
templateUrl: "fido2-use-browser-link.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule],
|
||||||
animations: [
|
animations: [
|
||||||
trigger("transformPanel", [
|
trigger("transformPanel", [
|
||||||
state(
|
state(
|
||||||
@@ -90,11 +95,11 @@ export class Fido2UseBrowserLinkComponent {
|
|||||||
* @param uri - The domain uri to exclude from future FIDO2 prompts.
|
* @param uri - The domain uri to exclude from future FIDO2 prompts.
|
||||||
*/
|
*/
|
||||||
private async handleDomainExclusion(uri: string) {
|
private async handleDomainExclusion(uri: string) {
|
||||||
const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
|
const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
|
||||||
|
|
||||||
const validDomain = Utils.getHostname(uri);
|
const validDomain = Utils.getHostname(uri);
|
||||||
const savedDomains: NeverDomains = {
|
const savedDomains: NeverDomains = {
|
||||||
...exisitingDomains,
|
...existingDomains,
|
||||||
};
|
};
|
||||||
savedDomains[validDomain] = null;
|
savedDomains[validDomain] = null;
|
||||||
|
|
||||||
|
|||||||
142
apps/browser/src/autofill/popup/fido2/fido2-v1.component.html
Normal file
142
apps/browser/src/autofill/popup/fido2/fido2-v1.component.html
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<ng-container *ngIf="data$ | async as data">
|
||||||
|
<div class="auth-wrapper">
|
||||||
|
<div class="auth-header">
|
||||||
|
<div class="left">
|
||||||
|
<ng-container *ngIf="data.message.type != BrowserFido2MessageTypes.PickCredentialRequest">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="bwi bwi-shield"></i>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="data.message.type === BrowserFido2MessageTypes.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 === BrowserFido2MessageTypes.ConfirmNewCredentialRequest"
|
||||||
|
>
|
||||||
|
<div class="search">
|
||||||
|
<input
|
||||||
|
type="{{ searchTypeSearch ? 'search' : 'text' }}"
|
||||||
|
placeholder="{{ 'searchVault' | i18n }}"
|
||||||
|
id="search"
|
||||||
|
[(ngModel)]="searchText"
|
||||||
|
(input)="search()"
|
||||||
|
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 === BrowserFido2MessageTypes.PickCredentialRequest ||
|
||||||
|
data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<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-v1
|
||||||
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
|
[cipher]="cipherItem"
|
||||||
|
[isSearching]="searchPending"
|
||||||
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
|
(onSelected)="selectedPasskey($event)"
|
||||||
|
[isSelected]="cipher === cipherItem"
|
||||||
|
></app-fido2-cipher-row-v1>
|
||||||
|
</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 === BrowserFido2MessageTypes.InformExcludedCredentialRequest"
|
||||||
|
>
|
||||||
|
<div class="auth-flow">
|
||||||
|
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
|
||||||
|
<div class="box list">
|
||||||
|
<div class="box-content">
|
||||||
|
<app-fido2-cipher-row-v1
|
||||||
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
|
[cipher]="cipherItem"
|
||||||
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
|
(onSelected)="selectedPasskey($event)"
|
||||||
|
[isSelected]="cipher === cipherItem"
|
||||||
|
></app-fido2-cipher-row-v1>
|
||||||
|
</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 === BrowserFido2MessageTypes.InformCredentialNotFoundRequest"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<app-fido2-use-browser-link-v1></app-fido2-use-browser-link-v1>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
443
apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts
Normal file
443
apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
Subject,
|
||||||
|
take,
|
||||||
|
takeUntil,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-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 { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
|
||||||
|
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
|
||||||
|
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
|
||||||
|
import {
|
||||||
|
BrowserFido2Message,
|
||||||
|
BrowserFido2UserInterfaceSession,
|
||||||
|
BrowserFido2MessageTypes,
|
||||||
|
} from "../../fido2/services/browser-fido2-user-interface.service";
|
||||||
|
|
||||||
|
interface ViewData {
|
||||||
|
message: BrowserFido2Message;
|
||||||
|
fallbackSupported: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-fido2-v1",
|
||||||
|
templateUrl: "fido2-v1.component.html",
|
||||||
|
styleUrls: [],
|
||||||
|
})
|
||||||
|
export class Fido2V1Component implements OnInit, OnDestroy {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private hasSearched = 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;
|
||||||
|
protected BrowserFido2MessageTypes = BrowserFido2MessageTypes;
|
||||||
|
|
||||||
|
private message$ = new BehaviorSubject<BrowserFido2Message>(null);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
private logService: LogService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private browserMessagingApi: ZonedMessageListenerService,
|
||||||
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
|
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||||
|
|
||||||
|
const queryParams$ = this.activatedRoute.queryParamMap.pipe(
|
||||||
|
take(1),
|
||||||
|
map((queryParamMap) => ({
|
||||||
|
sessionId: queryParamMap.get("sessionId"),
|
||||||
|
senderTabId: queryParamMap.get("senderTabId"),
|
||||||
|
senderUrl: queryParamMap.get("senderUrl"),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
queryParams$,
|
||||||
|
this.browserMessagingApi.messageListener$() as Observable<BrowserFido2Message>,
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
concatMap(async ([queryParams, message]) => {
|
||||||
|
this.sessionId = queryParams.sessionId;
|
||||||
|
this.senderTabId = queryParams.senderTabId;
|
||||||
|
this.url = queryParams.senderUrl;
|
||||||
|
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
|
||||||
|
if (
|
||||||
|
message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest &&
|
||||||
|
message.sessionId !== queryParams.sessionId
|
||||||
|
) {
|
||||||
|
this.abort(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore messages that don't belong to the current session.
|
||||||
|
if (message.sessionId !== queryParams.sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === BrowserFido2MessageTypes.AbortRequest) {
|
||||||
|
this.abort(false);
|
||||||
|
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) => {
|
||||||
|
switch (message.type) {
|
||||||
|
case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: {
|
||||||
|
const equivalentDomains = await firstValueFrom(
|
||||||
|
this.domainSettingsService.getUrlEquivalentDomains(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) &&
|
||||||
|
this.hasNoOtherPasskeys(cipher, message.userHandle),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.displayedCiphers.length > 0) {
|
||||||
|
this.selectedPasskey(this.displayedCiphers[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrowserFido2MessageTypes.PickCredentialRequest: {
|
||||||
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
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, activeUserId),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.displayedCiphers = [...this.ciphers];
|
||||||
|
if (this.displayedCiphers.length > 0) {
|
||||||
|
this.selectedPasskey(this.displayedCiphers[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BrowserFido2MessageTypes.InformExcludedCredentialRequest: {
|
||||||
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
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, activeUserId),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
);
|
||||||
|
|
||||||
|
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
|
||||||
|
this.send({
|
||||||
|
sessionId: queryParams.sessionId,
|
||||||
|
type: BrowserFido2MessageTypes.ConnectResponse,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async submit() {
|
||||||
|
const data = this.message$.value;
|
||||||
|
if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) {
|
||||||
|
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
||||||
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
|
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
||||||
|
|
||||||
|
this.send({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
cipherId: this.cipher.id,
|
||||||
|
type: BrowserFido2MessageTypes.PickCredentialResponse,
|
||||||
|
userVerified,
|
||||||
|
});
|
||||||
|
} else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
|
if (this.cipher.login.hasFido2Credentials) {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "overwritePasskey" },
|
||||||
|
content: { key: "overwritePasskeyAlert" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
||||||
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
|
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
||||||
|
|
||||||
|
this.send({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
cipherId: this.cipher.id,
|
||||||
|
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||||
|
userVerified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async saveNewLogin() {
|
||||||
|
const data = this.message$.value;
|
||||||
|
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
|
const name = data.credentialName || data.rpId;
|
||||||
|
// TODO: Revert to check for user verification once user verification for passkeys is approved for production.
|
||||||
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
|
await this.createNewCipher(name, data.userName);
|
||||||
|
|
||||||
|
// We are bypassing user verification pending approval.
|
||||||
|
this.send({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
cipherId: this.cipher?.id,
|
||||||
|
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||||
|
userVerified: data.userVerification,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCredentialSubTitleText(messageType: string): string {
|
||||||
|
return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
|
||||||
|
? "chooseCipherForPasskeySave"
|
||||||
|
: "logInWithPasskeyQuestion";
|
||||||
|
}
|
||||||
|
|
||||||
|
getCredentialButtonText(messageType: string): string {
|
||||||
|
return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
|
||||||
|
? "savePasskey"
|
||||||
|
: "confirm";
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPasskey(item: CipherView) {
|
||||||
|
this.cipher = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPasskey() {
|
||||||
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.router.navigate(["/view-cipher"], {
|
||||||
|
queryParams: {
|
||||||
|
cipherId: this.cipher.id,
|
||||||
|
uilocation: "popout",
|
||||||
|
senderTabId: this.senderTabId,
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addCipher() {
|
||||||
|
const data = this.message$.value;
|
||||||
|
|
||||||
|
if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.router.navigate(["/add-cipher"], {
|
||||||
|
queryParams: {
|
||||||
|
name: data.credentialName || data.rpId,
|
||||||
|
uri: this.url,
|
||||||
|
type: CipherType.Login.toString(),
|
||||||
|
uilocation: "popout",
|
||||||
|
username: data.userName,
|
||||||
|
senderTabId: this.senderTabId,
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
userVerification: data.userVerification,
|
||||||
|
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async search() {
|
||||||
|
this.hasSearched = await this.searchService.isSearchable(this.searchText);
|
||||||
|
this.searchPending = true;
|
||||||
|
if (this.hasSearched) {
|
||||||
|
this.displayedCiphers = await this.searchService.searchCiphers(
|
||||||
|
this.searchText,
|
||||||
|
null,
|
||||||
|
this.ciphers,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const equivalentDomains = await firstValueFrom(
|
||||||
|
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||||
|
);
|
||||||
|
this.displayedCiphers = this.ciphers.filter((cipher) =>
|
||||||
|
cipher.login.matchesUri(this.url, equivalentDomains),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.searchPending = false;
|
||||||
|
this.selectedPasskey(this.displayedCiphers[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(fallback: boolean) {
|
||||||
|
this.unload(fallback);
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
unload(fallback = false) {
|
||||||
|
this.send({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
type: BrowserFido2MessageTypes.AbortResponse,
|
||||||
|
fallbackRequested: fallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCipher(name: string, username: string) {
|
||||||
|
this.cipher = new CipherView();
|
||||||
|
this.cipher.name = name;
|
||||||
|
|
||||||
|
this.cipher.type = CipherType.Login;
|
||||||
|
this.cipher.login = new LoginView();
|
||||||
|
this.cipher.login.username = username;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createNewCipher(name: string, username: string) {
|
||||||
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.buildCipher(name, username);
|
||||||
|
const cipher = await this.cipherService.encrypt(this.cipher, activeUserId);
|
||||||
|
try {
|
||||||
|
await this.cipherService.createWithServer(cipher);
|
||||||
|
this.cipher.id = cipher.id;
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production.
|
||||||
|
private async handleUserVerification(
|
||||||
|
userVerificationRequested: boolean,
|
||||||
|
cipher: CipherView,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0;
|
||||||
|
|
||||||
|
if (masterPasswordRepromptRequired) {
|
||||||
|
return await this.passwordRepromptService.showPasswordPrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return userVerificationRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
private send(msg: BrowserFido2Message) {
|
||||||
|
BrowserFido2UserInterfaceSession.sendMessage({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
...msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
|
||||||
|
* @param userHandle
|
||||||
|
*/
|
||||||
|
private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
|
||||||
|
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,106 +1,74 @@
|
|||||||
<ng-container *ngIf="data$ | async as data">
|
<popup-page *ngIf="data$ | async as data">
|
||||||
<div class="auth-wrapper">
|
<popup-header
|
||||||
<div class="auth-header">
|
slot="header"
|
||||||
<div class="left">
|
pageTitle="{{
|
||||||
<ng-container *ngIf="data.message.type != 'PickCredentialRequest'">
|
(passkeyAction === PasskeyActions.Register ? 'savePasskey' : 'logInWithPasskeyQuestion')
|
||||||
<div class="logo">
|
| i18n
|
||||||
<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()"
|
|
||||||
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'
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div class="auth-flow">
|
<button
|
||||||
<p class="subtitle" appA11yTitle="{{ subtitleText | i18n }}">
|
*ngIf="showNewPasskeyButton"
|
||||||
{{ subtitleText | i18n }}
|
bitButton
|
||||||
</p>
|
buttonType="primary"
|
||||||
<!-- Display when ciphers exist -->
|
type="button"
|
||||||
|
(click)="addCipher()"
|
||||||
|
slot="end"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||||
|
{{ "new" | i18n }}
|
||||||
|
</button>
|
||||||
|
</popup-header>
|
||||||
|
|
||||||
|
<div class="tw-p-2">
|
||||||
|
<bit-section *ngIf="passkeyAction === PasskeyActions.Register">
|
||||||
|
<bit-search
|
||||||
|
appAutofocus
|
||||||
|
autocomplete="off"
|
||||||
|
id="search"
|
||||||
|
placeholder="{{ 'searchVault' | i18n }}"
|
||||||
|
(ngModelChange)="search()"
|
||||||
|
[(ngModel)]="searchText"
|
||||||
|
></bit-search>
|
||||||
|
</bit-section>
|
||||||
|
|
||||||
|
<!-- Display when adding a new passkey -->
|
||||||
|
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest">
|
||||||
|
<!-- Display when matching ciphers (i.e. same domain, no passkeys) exist -->
|
||||||
<ng-container *ngIf="displayedCiphers.length > 0">
|
<ng-container *ngIf="displayedCiphers.length > 0">
|
||||||
<div class="box list">
|
<bit-section-header>
|
||||||
<div class="box-content">
|
<h2 bitTypography="h6">{{ "chooseCipherForPasskeySave" | i18n }}</h2>
|
||||||
|
</bit-section-header>
|
||||||
<app-fido2-cipher-row
|
<app-fido2-cipher-row
|
||||||
*ngFor="let cipherItem of displayedCiphers"
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
[cipher]="cipherItem"
|
[cipher]="cipherItem"
|
||||||
[isSearching]="searchPending"
|
|
||||||
title="{{ 'passkeyItem' | i18n }}"
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
(onSelected)="selectedPasskey($event)"
|
(onSelected)="handleCipherItemSelect($event)"
|
||||||
[isSelected]="cipher === cipherItem"
|
|
||||||
></app-fido2-cipher-row>
|
></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>
|
||||||
|
|
||||||
|
<!-- Display when no matching ciphers exist -->
|
||||||
<ng-container *ngIf="!displayedCiphers.length">
|
<ng-container *ngIf="!displayedCiphers.length">
|
||||||
<div class="box">
|
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
|
||||||
|
<ng-container slot="title">{{ "noMatchingLoginsForSite" | i18n }}</ng-container>
|
||||||
|
<ng-container slot="description">Search or save passkey as new login</ng-container>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
slot="button"
|
||||||
|
type="button"
|
||||||
(click)="saveNewLogin()"
|
(click)="saveNewLogin()"
|
||||||
class="btn primary block"
|
[loading]="loading"
|
||||||
appA11yTitle="{{ 'savePasskeyNewLogin' | i18n }}"
|
|
||||||
>
|
>
|
||||||
<span [hidden]="loading">
|
|
||||||
{{ "savePasskeyNewLogin" | i18n }}
|
{{ "savePasskeyNewLogin" | i18n }}
|
||||||
</span>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-lg bwi-spin"
|
|
||||||
[hidden]="!loading"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</bit-no-items>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</bit-section>
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="data.message.type === 'InformExcludedCredentialRequest'">
|
<!-- Display when the passkey being saved already exists -->
|
||||||
|
<bit-section
|
||||||
|
*ngIf="data.message.type === BrowserFido2MessageTypes.InformExcludedCredentialRequest"
|
||||||
|
>
|
||||||
<div class="auth-flow">
|
<div class="auth-flow">
|
||||||
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
|
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
|
||||||
<div class="box list">
|
<div class="box list">
|
||||||
@@ -109,18 +77,49 @@
|
|||||||
*ngFor="let cipherItem of displayedCiphers"
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
[cipher]="cipherItem"
|
[cipher]="cipherItem"
|
||||||
title="{{ 'passkeyItem' | i18n }}"
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
(onSelected)="selectedPasskey($event)"
|
(onSelected)="handleCipherItemSelect($event)"
|
||||||
[isSelected]="cipher === cipherItem"
|
|
||||||
></app-fido2-cipher-row>
|
></app-fido2-cipher-row>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
</bit-section>
|
||||||
|
|
||||||
|
<!-- Display when picking a passkey to login with -->
|
||||||
|
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.PickCredentialRequest">
|
||||||
|
<!-- Display when matching ciphers exist -->
|
||||||
|
<ng-container *ngIf="displayedCiphers.length > 0">
|
||||||
|
<ng-container slot="title">{{ "chooseCipherForPasskeyAuth" | i18n }}</ng-container>
|
||||||
|
<app-fido2-cipher-row
|
||||||
|
*ngFor="let cipherItem of displayedCiphers"
|
||||||
|
[cipher]="cipherItem"
|
||||||
|
title="{{ 'passkeyItem' | i18n }}"
|
||||||
|
(onSelected)="handleCipherItemSelect($event)"
|
||||||
|
></app-fido2-cipher-row>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="data.message.type === 'InformCredentialNotFoundRequest'">
|
|
||||||
|
<!-- Display when no matching ciphers exist -->
|
||||||
|
<ng-container *ngIf="!displayedCiphers.length">
|
||||||
|
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
|
||||||
|
<ng-container slot="title">No matching logins for this site</ng-container>
|
||||||
|
<ng-container slot="description">Search or save passkey as new login</ng-container>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
slot="button"
|
||||||
|
type="button"
|
||||||
|
(click)="saveNewLogin()"
|
||||||
|
[loading]="loading"
|
||||||
|
>
|
||||||
|
{{ "savePasskeyNewLogin" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-no-items>
|
||||||
|
</ng-container>
|
||||||
|
</bit-section>
|
||||||
|
|
||||||
|
<!-- Display when initiating passkey login, but no cooresponding cipher is found in the vault -->
|
||||||
|
<bit-section
|
||||||
|
*ngIf="data.message.type === BrowserFido2MessageTypes.InformCredentialNotFoundRequest"
|
||||||
|
>
|
||||||
<div class="auth-flow">
|
<div class="auth-flow">
|
||||||
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
|
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,9 +127,8 @@
|
|||||||
<span [hidden]="loading">{{ "close" | i18n }}</span>
|
<span [hidden]="loading">{{ "close" | i18n }}</span>
|
||||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</bit-section>
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<app-fido2-use-browser-link></app-fido2-use-browser-link>
|
<app-fido2-use-browser-link></app-fido2-use-browser-link>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</popup-page>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FormsModule } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
@@ -13,13 +15,14 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
import { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -27,17 +30,39 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"
|
|||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import {
|
||||||
|
ButtonModule,
|
||||||
|
DialogService,
|
||||||
|
Icons,
|
||||||
|
ItemModule,
|
||||||
|
NoItemsModule,
|
||||||
|
SearchModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
} from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
|
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
|
||||||
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
|
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
|
||||||
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
|
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
|
||||||
import {
|
import {
|
||||||
BrowserFido2Message,
|
BrowserFido2Message,
|
||||||
BrowserFido2UserInterfaceSession,
|
BrowserFido2UserInterfaceSession,
|
||||||
|
BrowserFido2MessageTypes,
|
||||||
} from "../../fido2/services/browser-fido2-user-interface.service";
|
} from "../../fido2/services/browser-fido2-user-interface.service";
|
||||||
|
|
||||||
|
import { Fido2CipherRowComponent } from "./fido2-cipher-row.component";
|
||||||
|
import { Fido2UseBrowserLinkComponent } from "./fido2-use-browser-link.component";
|
||||||
|
|
||||||
|
const PasskeyActions = {
|
||||||
|
Register: "register",
|
||||||
|
Authenticate: "authenticate",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type PasskeyActionValue = (typeof PasskeyActions)[keyof typeof PasskeyActions];
|
||||||
|
|
||||||
interface ViewData {
|
interface ViewData {
|
||||||
message: BrowserFido2Message;
|
message: BrowserFido2Message;
|
||||||
fallbackSupported: boolean;
|
fallbackSupported: boolean;
|
||||||
@@ -46,28 +71,45 @@ interface ViewData {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "app-fido2",
|
selector: "app-fido2",
|
||||||
templateUrl: "fido2.component.html",
|
templateUrl: "fido2.component.html",
|
||||||
styleUrls: [],
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
CommonModule,
|
||||||
|
Fido2CipherRowComponent,
|
||||||
|
Fido2UseBrowserLinkComponent,
|
||||||
|
FormsModule,
|
||||||
|
ItemModule,
|
||||||
|
JslibModule,
|
||||||
|
NoItemsModule,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopupPageComponent,
|
||||||
|
SearchModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class Fido2Component implements OnInit, OnDestroy {
|
export class Fido2Component implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private hasSearched = 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);
|
private message$ = new BehaviorSubject<BrowserFido2Message>(null);
|
||||||
|
private hasSearched = false;
|
||||||
|
protected BrowserFido2MessageTypes = BrowserFido2MessageTypes;
|
||||||
|
protected cipher: CipherView;
|
||||||
|
protected ciphers?: CipherView[] = [];
|
||||||
|
protected data$: Observable<ViewData>;
|
||||||
|
protected displayedCiphers?: CipherView[] = [];
|
||||||
|
protected equivalentDomains: Set<string>;
|
||||||
|
protected equivalentDomainsURL: string;
|
||||||
|
protected hostname: string;
|
||||||
|
protected loading = false;
|
||||||
|
protected noResultsIcon = Icons.NoResults;
|
||||||
|
protected passkeyAction: PasskeyActionValue = PasskeyActions.Register;
|
||||||
|
protected PasskeyActions = PasskeyActions;
|
||||||
|
protected searchText: string;
|
||||||
|
protected searchTypeSearch = false;
|
||||||
|
protected senderTabId?: string;
|
||||||
|
protected sessionId?: string;
|
||||||
|
protected showNewPasskeyButton: boolean = false;
|
||||||
|
protected url: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -80,8 +122,8 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private browserMessagingApi: ZonedMessageListenerService,
|
private browserMessagingApi: ZonedMessageListenerService,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
private fido2UserVerificationService: Fido2UserVerificationService,
|
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -107,7 +149,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.url = queryParams.senderUrl;
|
this.url = queryParams.senderUrl;
|
||||||
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
|
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
|
||||||
if (
|
if (
|
||||||
message.type === "NewSessionCreatedRequest" &&
|
message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest &&
|
||||||
message.sessionId !== queryParams.sessionId
|
message.sessionId !== queryParams.sessionId
|
||||||
) {
|
) {
|
||||||
this.abort(false);
|
this.abort(false);
|
||||||
@@ -119,7 +161,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "AbortRequest") {
|
if (message.type === BrowserFido2MessageTypes.AbortRequest) {
|
||||||
this.abort(false);
|
this.abort(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -137,7 +179,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
filter((message) => message != undefined),
|
filter((message) => message != undefined),
|
||||||
concatMap(async (message) => {
|
concatMap(async (message) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "ConfirmNewCredentialRequest": {
|
case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: {
|
||||||
const equivalentDomains = await firstValueFrom(
|
const equivalentDomains = await firstValueFrom(
|
||||||
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||||
);
|
);
|
||||||
@@ -145,19 +187,22 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||||
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
|
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.displayedCiphers = this.ciphers.filter(
|
this.displayedCiphers = this.ciphers.filter(
|
||||||
(cipher) =>
|
(cipher) =>
|
||||||
cipher.login.matchesUri(this.url, equivalentDomains) &&
|
cipher.login.matchesUri(this.url, equivalentDomains) &&
|
||||||
this.hasNoOtherPasskeys(cipher, message.userHandle),
|
this.cipherHasNoOtherPasskeys(cipher, message.userHandle),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.displayedCiphers.length > 0) {
|
this.passkeyAction = PasskeyActions.Register;
|
||||||
this.selectedPasskey(this.displayedCiphers[0]);
|
|
||||||
}
|
// @TODO fix new cipher creation for other fido2 registration message types and remove `showNewPasskeyButton` from the template
|
||||||
|
this.showNewPasskeyButton = true;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "PickCredentialRequest": {
|
case BrowserFido2MessageTypes.PickCredentialRequest: {
|
||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
@@ -170,14 +215,15 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.displayedCiphers = [...this.ciphers];
|
this.displayedCiphers = [...this.ciphers];
|
||||||
if (this.displayedCiphers.length > 0) {
|
|
||||||
this.selectedPasskey(this.displayedCiphers[0]);
|
this.passkeyAction = PasskeyActions.Authenticate;
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "InformExcludedCredentialRequest": {
|
case BrowserFido2MessageTypes.InformExcludedCredentialRequest: {
|
||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
@@ -190,40 +236,42 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.displayedCiphers = [...this.ciphers];
|
this.displayedCiphers = [...this.ciphers];
|
||||||
|
|
||||||
if (this.displayedCiphers.length > 0) {
|
this.passkeyAction = PasskeyActions.Register;
|
||||||
this.selectedPasskey(this.displayedCiphers[0]);
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case BrowserFido2MessageTypes.InformCredentialNotFoundRequest: {
|
||||||
|
this.passkeyAction = PasskeyActions.Authenticate;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.subtitleText =
|
|
||||||
this.displayedCiphers.length > 0
|
|
||||||
? this.getCredentialSubTitleText(message.type)
|
|
||||||
: "noMatchingPasskeyLogin";
|
|
||||||
|
|
||||||
this.credentialText = this.getCredentialButtonText(message.type);
|
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
|
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
);
|
);
|
||||||
|
|
||||||
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
|
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: queryParams.sessionId,
|
sessionId: queryParams.sessionId,
|
||||||
type: "ConnectResponse",
|
type: BrowserFido2MessageTypes.ConnectResponse,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async submit() {
|
protected async submit() {
|
||||||
const data = this.message$.value;
|
const data = this.message$.value;
|
||||||
if (data?.type === "PickCredentialRequest") {
|
|
||||||
|
if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) {
|
||||||
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
||||||
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
||||||
@@ -231,10 +279,10 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
cipherId: this.cipher.id,
|
cipherId: this.cipher.id,
|
||||||
type: "PickCredentialResponse",
|
type: BrowserFido2MessageTypes.PickCredentialResponse,
|
||||||
userVerified,
|
userVerified,
|
||||||
});
|
});
|
||||||
} else if (data?.type === "ConfirmNewCredentialRequest") {
|
} else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
if (this.cipher.login.hasFido2Credentials) {
|
if (this.cipher.login.hasFido2Credentials) {
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "overwritePasskey" },
|
title: { key: "overwritePasskey" },
|
||||||
@@ -254,7 +302,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
cipherId: this.cipher.id,
|
cipherId: this.cipher.id,
|
||||||
type: "ConfirmNewCredentialResponse",
|
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||||
userVerified,
|
userVerified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -264,7 +312,8 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
protected async saveNewLogin() {
|
protected async saveNewLogin() {
|
||||||
const data = this.message$.value;
|
const data = this.message$.value;
|
||||||
if (data?.type === "ConfirmNewCredentialRequest") {
|
|
||||||
|
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
const name = data.credentialName || data.rpId;
|
const name = data.credentialName || data.rpId;
|
||||||
// TODO: Revert to check for user verification once user verification for passkeys is approved for production.
|
// TODO: Revert to check for user verification once user verification for passkeys is approved for production.
|
||||||
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||||
@@ -274,7 +323,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
cipherId: this.cipher?.id,
|
cipherId: this.cipher?.id,
|
||||||
type: "ConfirmNewCredentialResponse",
|
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||||
userVerified: data.userVerification,
|
userVerified: data.userVerification,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -282,46 +331,21 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredentialSubTitleText(messageType: string): string {
|
async handleCipherItemSelect(item: CipherView) {
|
||||||
return messageType == "ConfirmNewCredentialRequest" ? "choosePasskey" : "logInWithPasskey";
|
|
||||||
}
|
|
||||||
|
|
||||||
getCredentialButtonText(messageType: string): string {
|
|
||||||
return messageType == "ConfirmNewCredentialRequest" ? "savePasskey" : "confirm";
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedPasskey(item: CipherView) {
|
|
||||||
this.cipher = item;
|
this.cipher = item;
|
||||||
|
|
||||||
|
await this.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
viewPasskey() {
|
async addCipher() {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/view-cipher"], {
|
|
||||||
queryParams: {
|
|
||||||
cipherId: this.cipher.id,
|
|
||||||
uilocation: "popout",
|
|
||||||
senderTabId: this.senderTabId,
|
|
||||||
sessionId: this.sessionId,
|
|
||||||
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addCipher() {
|
|
||||||
const data = this.message$.value;
|
const data = this.message$.value;
|
||||||
|
|
||||||
if (data?.type !== "ConfirmNewCredentialRequest") {
|
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||||
return;
|
await this.router.navigate(["/add-cipher"], {
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/add-cipher"], {
|
|
||||||
queryParams: {
|
queryParams: {
|
||||||
|
type: CipherType.Login.toString(),
|
||||||
name: data.credentialName || data.rpId,
|
name: data.credentialName || data.rpId,
|
||||||
uri: this.url,
|
uri: this.url,
|
||||||
type: CipherType.Login.toString(),
|
|
||||||
uilocation: "popout",
|
uilocation: "popout",
|
||||||
username: data.userName,
|
username: data.userName,
|
||||||
senderTabId: this.senderTabId,
|
senderTabId: this.senderTabId,
|
||||||
@@ -332,9 +356,22 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEquivalentDomains() {
|
||||||
|
if (this.equivalentDomainsURL !== this.url) {
|
||||||
|
this.equivalentDomainsURL = this.url;
|
||||||
|
this.equivalentDomains = await firstValueFrom(
|
||||||
|
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.equivalentDomains;
|
||||||
|
}
|
||||||
|
|
||||||
protected async search() {
|
protected async search() {
|
||||||
this.hasSearched = await this.searchService.isSearchable(this.searchText);
|
this.hasSearched = await this.searchService.isSearchable(this.searchText);
|
||||||
this.searchPending = true;
|
|
||||||
if (this.hasSearched) {
|
if (this.hasSearched) {
|
||||||
this.displayedCiphers = await this.searchService.searchCiphers(
|
this.displayedCiphers = await this.searchService.searchCiphers(
|
||||||
this.searchText,
|
this.searchText,
|
||||||
@@ -342,15 +379,11 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.ciphers,
|
this.ciphers,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const equivalentDomains = await firstValueFrom(
|
const equivalentDomains = await this.getEquivalentDomains();
|
||||||
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
|
||||||
);
|
|
||||||
this.displayedCiphers = this.ciphers.filter((cipher) =>
|
this.displayedCiphers = this.ciphers.filter((cipher) =>
|
||||||
cipher.login.matchesUri(this.url, equivalentDomains),
|
cipher.login.matchesUri(this.url, equivalentDomains),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.searchPending = false;
|
|
||||||
this.selectedPasskey(this.displayedCiphers[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abort(fallback: boolean) {
|
abort(fallback: boolean) {
|
||||||
@@ -361,7 +394,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
unload(fallback = false) {
|
unload(fallback = false) {
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
type: "AbortResponse",
|
type: BrowserFido2MessageTypes.AbortResponse,
|
||||||
fallbackRequested: fallback,
|
fallbackRequested: fallback,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -427,13 +460,11 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
|
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
|
||||||
* @param userHandle
|
* @param userHandle
|
||||||
*/
|
*/
|
||||||
private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
|
private cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
|
||||||
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
|
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cipher.login.fido2Credentials.some((passkey) => {
|
return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
|
||||||
passkey.userHandle === userHandle;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"
|
|||||||
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
|
import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
|
||||||
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
||||||
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
@@ -127,12 +128,11 @@ const routes: Routes = [
|
|||||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||||
data: { state: "home" },
|
data: { state: "home" },
|
||||||
},
|
},
|
||||||
{
|
...extensionRefreshSwap(Fido2V1Component, Fido2Component, {
|
||||||
path: "fido2",
|
path: "fido2",
|
||||||
component: Fido2Component,
|
|
||||||
canActivate: [fido2AuthGuard],
|
canActivate: [fido2AuthGuard],
|
||||||
data: { state: "fido2" },
|
data: { state: "fido2" },
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
path: "login",
|
path: "login",
|
||||||
component: LoginComponent,
|
component: LoginComponent,
|
||||||
@@ -304,7 +304,6 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
|
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
|
||||||
path: "notifications",
|
path: "notifications",
|
||||||
component: NotificationsSettingsV1Component,
|
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "notifications" },
|
data: { state: "notifications" },
|
||||||
}),
|
}),
|
||||||
@@ -338,7 +337,6 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
||||||
path: "excluded-domains",
|
path: "excluded-domains",
|
||||||
component: ExcludedDomainsV1Component,
|
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "excluded-domains" },
|
data: { state: "excluded-domains" },
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -35,8 +35,11 @@ import { SsoComponent } from "../auth/popup/sso.component";
|
|||||||
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
|
import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component";
|
||||||
import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component";
|
import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component";
|
||||||
|
import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component";
|
||||||
import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component";
|
import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component";
|
||||||
|
import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
|
||||||
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
||||||
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
@@ -112,6 +115,9 @@ import "../platform/popup/locales";
|
|||||||
ServicesModule,
|
ServicesModule,
|
||||||
DialogModule,
|
DialogModule,
|
||||||
ExcludedDomainsComponent,
|
ExcludedDomainsComponent,
|
||||||
|
Fido2CipherRowComponent,
|
||||||
|
Fido2Component,
|
||||||
|
Fido2UseBrowserLinkComponent,
|
||||||
FilePopoutCalloutComponent,
|
FilePopoutCalloutComponent,
|
||||||
AvatarModule,
|
AvatarModule,
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
@@ -140,8 +146,8 @@ import "../platform/popup/locales";
|
|||||||
CurrentTabComponent,
|
CurrentTabComponent,
|
||||||
EnvironmentComponent,
|
EnvironmentComponent,
|
||||||
ExcludedDomainsV1Component,
|
ExcludedDomainsV1Component,
|
||||||
Fido2CipherRowComponent,
|
Fido2CipherRowV1Component,
|
||||||
Fido2UseBrowserLinkComponent,
|
Fido2UseBrowserLinkV1Component,
|
||||||
FolderAddEditComponent,
|
FolderAddEditComponent,
|
||||||
FoldersComponent,
|
FoldersComponent,
|
||||||
VaultFilterComponent,
|
VaultFilterComponent,
|
||||||
@@ -180,7 +186,7 @@ import "../platform/popup/locales";
|
|||||||
ViewCustomFieldsComponent,
|
ViewCustomFieldsComponent,
|
||||||
RemovePasswordComponent,
|
RemovePasswordComponent,
|
||||||
VaultSelectComponent,
|
VaultSelectComponent,
|
||||||
Fido2Component,
|
Fido2V1Component,
|
||||||
AutofillV1Component,
|
AutofillV1Component,
|
||||||
EnvironmentSelectorComponent,
|
EnvironmentSelectorComponent,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ app-vault-attachments {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app-fido2 {
|
app-fido2-v1 {
|
||||||
.auth-wrapper {
|
.auth-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user