mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-23 03:22:50 +02: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:
parent
44f1fc156c
commit
095ce7ec30
@ -3477,7 +3477,7 @@
|
||||
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
|
||||
"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?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
@ -3489,6 +3489,9 @@
|
||||
"noMatchingPasskeyLogin": {
|
||||
"message": "You do not have a matching login for this site."
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"confirm": {
|
||||
"message": "Confirm"
|
||||
},
|
||||
@ -3498,9 +3501,12 @@
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"choosePasskey": {
|
||||
"chooseCipherForPasskeySave": {
|
||||
"message": "Choose a login to save this passkey to"
|
||||
},
|
||||
"chooseCipherForPasskeyAuth": {
|
||||
"message": "Choose a passkey to log in with"
|
||||
},
|
||||
"passkeyItem": {
|
||||
"message": "Passkey Item"
|
||||
},
|
||||
|
@ -94,7 +94,7 @@
|
||||
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
|
||||
</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>
|
||||
</main>
|
||||
</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
|
||||
* 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
|
||||
* 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
|
||||
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>
|
||||
<bit-item>
|
||||
<button
|
||||
(click)="selectCipher(cipher)"
|
||||
appA11yTitle="{{ title }} - {{ cipher.name }}"
|
||||
bit-item-content
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||
<span data-testid="item-name">
|
||||
{{ cipher.name }}
|
||||
<i
|
||||
*ngIf="cipher.organizationId"
|
||||
[appA11yTitle]="'shared' | i18n"
|
||||
class="bwi bwi-collection text-muted"
|
||||
></i>
|
||||
</span>
|
||||
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
|
||||
<span *ngIf="cipher.subTitle" slot="secondary">{{ cipher.subTitle }}</span>
|
||||
</button>
|
||||
</bit-item>
|
||||
|
@ -1,19 +1,40 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
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 {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-fido2-cipher-row",
|
||||
templateUrl: "fido2-cipher-row.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
CommonModule,
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
JslibModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class Fido2CipherRowComponent {
|
||||
@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);
|
||||
|
@ -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 { 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 { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
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";
|
||||
@ -15,6 +18,8 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f
|
||||
@Component({
|
||||
selector: "app-fido2-use-browser-link",
|
||||
templateUrl: "fido2-use-browser-link.component.html",
|
||||
standalone: true,
|
||||
imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule],
|
||||
animations: [
|
||||
trigger("transformPanel", [
|
||||
state(
|
||||
@ -90,11 +95,11 @@ export class Fido2UseBrowserLinkComponent {
|
||||
* @param uri - The domain uri to exclude from future FIDO2 prompts.
|
||||
*/
|
||||
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 savedDomains: NeverDomains = {
|
||||
...exisitingDomains,
|
||||
...existingDomains,
|
||||
};
|
||||
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,136 +1,134 @@
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
<div class="auth-wrapper">
|
||||
<div class="auth-header">
|
||||
<div class="left">
|
||||
<ng-container *ngIf="data.message.type != 'PickCredentialRequest'">
|
||||
<div class="logo">
|
||||
<i class="bwi bwi-shield"></i>
|
||||
<popup-page *ngIf="data$ | async as data">
|
||||
<popup-header
|
||||
slot="header"
|
||||
pageTitle="{{
|
||||
(passkeyAction === PasskeyActions.Register ? 'savePasskey' : 'logInWithPasskeyQuestion')
|
||||
| i18n
|
||||
}}"
|
||||
>
|
||||
<button
|
||||
*ngIf="showNewPasskeyButton"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
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">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "chooseCipherForPasskeySave" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<app-fido2-cipher-row
|
||||
*ngFor="let cipherItem of displayedCiphers"
|
||||
[cipher]="cipherItem"
|
||||
title="{{ 'passkeyItem' | i18n }}"
|
||||
(onSelected)="handleCipherItemSelect($event)"
|
||||
></app-fido2-cipher-row>
|
||||
</ng-container>
|
||||
|
||||
<!-- Display when no matching ciphers exist -->
|
||||
<ng-container *ngIf="!displayedCiphers.length">
|
||||
<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
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
slot="button"
|
||||
type="button"
|
||||
(click)="saveNewLogin()"
|
||||
[loading]="loading"
|
||||
>
|
||||
{{ "savePasskeyNewLogin" | i18n }}
|
||||
</button>
|
||||
</bit-no-items>
|
||||
</ng-container>
|
||||
</bit-section>
|
||||
|
||||
<!-- Display when the passkey being saved already exists -->
|
||||
<bit-section
|
||||
*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
|
||||
*ngFor="let cipherItem of displayedCiphers"
|
||||
[cipher]="cipherItem"
|
||||
title="{{ 'passkeyItem' | i18n }}"
|
||||
(onSelected)="handleCipherItemSelect($event)"
|
||||
></app-fido2-cipher-row>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</bit-no-items>
|
||||
</ng-container>
|
||||
</div>
|
||||
</bit-section>
|
||||
|
||||
<ng-container>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
data.message.type === 'PickCredentialRequest' ||
|
||||
data.message.type === '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
|
||||
*ngFor="let cipherItem of displayedCiphers"
|
||||
[cipher]="cipherItem"
|
||||
[isSearching]="searchPending"
|
||||
title="{{ 'passkeyItem' | 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'">
|
||||
<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="{{ 'passkeyItem' | 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'">
|
||||
<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>
|
||||
<!-- 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">
|
||||
<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>
|
||||
</bit-section>
|
||||
|
||||
<app-fido2-use-browser-link></app-fido2-use-browser-link>
|
||||
</div>
|
||||
</ng-container>
|
||||
</popup-page>
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
@ -13,13 +15,14 @@ import {
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
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 { SecureNoteType, CipherType } 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";
|
||||
@ -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 { 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 {
|
||||
ButtonModule,
|
||||
DialogService,
|
||||
Icons,
|
||||
ItemModule,
|
||||
NoItemsModule,
|
||||
SearchModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
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 { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
|
||||
import {
|
||||
BrowserFido2Message,
|
||||
BrowserFido2UserInterfaceSession,
|
||||
BrowserFido2MessageTypes,
|
||||
} 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 {
|
||||
message: BrowserFido2Message;
|
||||
fallbackSupported: boolean;
|
||||
@ -46,28 +71,45 @@ interface ViewData {
|
||||
@Component({
|
||||
selector: "app-fido2",
|
||||
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 {
|
||||
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 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(
|
||||
private router: Router,
|
||||
@ -80,8 +122,8 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
private dialogService: DialogService,
|
||||
private browserMessagingApi: ZonedMessageListenerService,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||
private accountService: AccountService,
|
||||
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@ -107,7 +149,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
this.url = queryParams.senderUrl;
|
||||
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
|
||||
if (
|
||||
message.type === "NewSessionCreatedRequest" &&
|
||||
message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest &&
|
||||
message.sessionId !== queryParams.sessionId
|
||||
) {
|
||||
this.abort(false);
|
||||
@ -119,7 +161,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "AbortRequest") {
|
||||
if (message.type === BrowserFido2MessageTypes.AbortRequest) {
|
||||
this.abort(false);
|
||||
return;
|
||||
}
|
||||
@ -137,7 +179,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
filter((message) => message != undefined),
|
||||
concatMap(async (message) => {
|
||||
switch (message.type) {
|
||||
case "ConfirmNewCredentialRequest": {
|
||||
case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: {
|
||||
const equivalentDomains = await firstValueFrom(
|
||||
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||
);
|
||||
@ -145,19 +187,22 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
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),
|
||||
this.cipherHasNoOtherPasskeys(cipher, message.userHandle),
|
||||
);
|
||||
|
||||
if (this.displayedCiphers.length > 0) {
|
||||
this.selectedPasskey(this.displayedCiphers[0]);
|
||||
}
|
||||
this.passkeyAction = PasskeyActions.Register;
|
||||
|
||||
// @TODO fix new cipher creation for other fido2 registration message types and remove `showNewPasskeyButton` from the template
|
||||
this.showNewPasskeyButton = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "PickCredentialRequest": {
|
||||
case BrowserFido2MessageTypes.PickCredentialRequest: {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
@ -170,14 +215,15 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
this.displayedCiphers = [...this.ciphers];
|
||||
if (this.displayedCiphers.length > 0) {
|
||||
this.selectedPasskey(this.displayedCiphers[0]);
|
||||
}
|
||||
|
||||
this.passkeyAction = PasskeyActions.Authenticate;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "InformExcludedCredentialRequest": {
|
||||
case BrowserFido2MessageTypes.InformExcludedCredentialRequest: {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
@ -190,40 +236,42 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
this.displayedCiphers = [...this.ciphers];
|
||||
|
||||
if (this.displayedCiphers.length > 0) {
|
||||
this.selectedPasskey(this.displayedCiphers[0]);
|
||||
}
|
||||
this.passkeyAction = PasskeyActions.Register;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case BrowserFido2MessageTypes.InformCredentialNotFoundRequest: {
|
||||
this.passkeyAction = PasskeyActions.Authenticate;
|
||||
|
||||
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: "ConnectResponse",
|
||||
type: BrowserFido2MessageTypes.ConnectResponse,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected async submit() {
|
||||
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.
|
||||
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
||||
@ -231,10 +279,10 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
this.send({
|
||||
sessionId: this.sessionId,
|
||||
cipherId: this.cipher.id,
|
||||
type: "PickCredentialResponse",
|
||||
type: BrowserFido2MessageTypes.PickCredentialResponse,
|
||||
userVerified,
|
||||
});
|
||||
} else if (data?.type === "ConfirmNewCredentialRequest") {
|
||||
} else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||
if (this.cipher.login.hasFido2Credentials) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "overwritePasskey" },
|
||||
@ -254,7 +302,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
this.send({
|
||||
sessionId: this.sessionId,
|
||||
cipherId: this.cipher.id,
|
||||
type: "ConfirmNewCredentialResponse",
|
||||
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||
userVerified,
|
||||
});
|
||||
}
|
||||
@ -264,7 +312,8 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
|
||||
protected async saveNewLogin() {
|
||||
const data = this.message$.value;
|
||||
if (data?.type === "ConfirmNewCredentialRequest") {
|
||||
|
||||
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
|
||||
@ -274,7 +323,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
this.send({
|
||||
sessionId: this.sessionId,
|
||||
cipherId: this.cipher?.id,
|
||||
type: "ConfirmNewCredentialResponse",
|
||||
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||
userVerified: data.userVerification,
|
||||
});
|
||||
}
|
||||
@ -282,59 +331,47 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
getCredentialSubTitleText(messageType: string): string {
|
||||
return messageType == "ConfirmNewCredentialRequest" ? "choosePasskey" : "logInWithPasskey";
|
||||
}
|
||||
|
||||
getCredentialButtonText(messageType: string): string {
|
||||
return messageType == "ConfirmNewCredentialRequest" ? "savePasskey" : "confirm";
|
||||
}
|
||||
|
||||
selectedPasskey(item: CipherView) {
|
||||
async handleCipherItemSelect(item: CipherView) {
|
||||
this.cipher = item;
|
||||
|
||||
await this.submit();
|
||||
}
|
||||
|
||||
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() {
|
||||
async addCipher() {
|
||||
const data = this.message$.value;
|
||||
|
||||
if (data?.type !== "ConfirmNewCredentialRequest") {
|
||||
return;
|
||||
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||
await this.router.navigate(["/add-cipher"], {
|
||||
queryParams: {
|
||||
type: CipherType.Login.toString(),
|
||||
name: data.credentialName || data.rpId,
|
||||
uri: this.url,
|
||||
uilocation: "popout",
|
||||
username: data.userName,
|
||||
senderTabId: this.senderTabId,
|
||||
sessionId: this.sessionId,
|
||||
userVerification: data.userVerification,
|
||||
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 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}`,
|
||||
},
|
||||
});
|
||||
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() {
|
||||
this.hasSearched = await this.searchService.isSearchable(this.searchText);
|
||||
this.searchPending = true;
|
||||
if (this.hasSearched) {
|
||||
this.displayedCiphers = await this.searchService.searchCiphers(
|
||||
this.searchText,
|
||||
@ -342,15 +379,11 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
this.ciphers,
|
||||
);
|
||||
} else {
|
||||
const equivalentDomains = await firstValueFrom(
|
||||
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||
);
|
||||
const equivalentDomains = await this.getEquivalentDomains();
|
||||
this.displayedCiphers = this.ciphers.filter((cipher) =>
|
||||
cipher.login.matchesUri(this.url, equivalentDomains),
|
||||
);
|
||||
}
|
||||
this.searchPending = false;
|
||||
this.selectedPasskey(this.displayedCiphers[0]);
|
||||
}
|
||||
|
||||
abort(fallback: boolean) {
|
||||
@ -361,7 +394,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
unload(fallback = false) {
|
||||
this.send({
|
||||
sessionId: this.sessionId,
|
||||
type: "AbortResponse",
|
||||
type: BrowserFido2MessageTypes.AbortResponse,
|
||||
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
|
||||
* @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) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return cipher.login.fido2Credentials.some((passkey) => {
|
||||
passkey.userHandle === userHandle;
|
||||
});
|
||||
return cipher.login.fido2Credentials.some((passkey) => 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 { TwoFactorComponent } from "../auth/popup/two-factor.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 { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||
@ -127,12 +128,11 @@ const routes: Routes = [
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: { state: "home" },
|
||||
},
|
||||
{
|
||||
...extensionRefreshSwap(Fido2V1Component, Fido2Component, {
|
||||
path: "fido2",
|
||||
component: Fido2Component,
|
||||
canActivate: [fido2AuthGuard],
|
||||
data: { state: "fido2" },
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "login",
|
||||
component: LoginComponent,
|
||||
@ -304,7 +304,6 @@ const routes: Routes = [
|
||||
},
|
||||
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
|
||||
path: "notifications",
|
||||
component: NotificationsSettingsV1Component,
|
||||
canActivate: [authGuard],
|
||||
data: { state: "notifications" },
|
||||
}),
|
||||
@ -338,7 +337,6 @@ const routes: Routes = [
|
||||
},
|
||||
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
||||
path: "excluded-domains",
|
||||
component: ExcludedDomainsV1Component,
|
||||
canActivate: [authGuard],
|
||||
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 { TwoFactorComponent } from "../auth/popup/two-factor.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 { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.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 { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||
@ -112,6 +115,9 @@ import "../platform/popup/locales";
|
||||
ServicesModule,
|
||||
DialogModule,
|
||||
ExcludedDomainsComponent,
|
||||
Fido2CipherRowComponent,
|
||||
Fido2Component,
|
||||
Fido2UseBrowserLinkComponent,
|
||||
FilePopoutCalloutComponent,
|
||||
AvatarModule,
|
||||
AccountComponent,
|
||||
@ -140,8 +146,8 @@ import "../platform/popup/locales";
|
||||
CurrentTabComponent,
|
||||
EnvironmentComponent,
|
||||
ExcludedDomainsV1Component,
|
||||
Fido2CipherRowComponent,
|
||||
Fido2UseBrowserLinkComponent,
|
||||
Fido2CipherRowV1Component,
|
||||
Fido2UseBrowserLinkV1Component,
|
||||
FolderAddEditComponent,
|
||||
FoldersComponent,
|
||||
VaultFilterComponent,
|
||||
@ -180,7 +186,7 @@ import "../platform/popup/locales";
|
||||
ViewCustomFieldsComponent,
|
||||
RemovePasswordComponent,
|
||||
VaultSelectComponent,
|
||||
Fido2Component,
|
||||
Fido2V1Component,
|
||||
AutofillV1Component,
|
||||
EnvironmentSelectorComponent,
|
||||
],
|
||||
|
@ -217,7 +217,7 @@ app-vault-attachments {
|
||||
}
|
||||
}
|
||||
|
||||
app-fido2 {
|
||||
app-fido2-v1 {
|
||||
.auth-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
Loading…
Reference in New Issue
Block a user