diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 857ad72aac5..ea9486379f8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2419,6 +2419,21 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, + "aliasDomain": { + "message": "Alias domain" + }, + "passwordRepromptDisabledAutofillOnPageLoad": { + "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + }, + "autofillOnPageLoadSetToDefault": { + "message": "Auto-fill on page load set to use default setting.", + "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + }, + "turnOffMasterPasswordPromptToEditField": { + "message": "Turn off master password re-prompt to edit this field", + "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." + }, "loginPasskey": { "message": "This login uses a passkey" }, @@ -2434,19 +2449,58 @@ "passkeyNotCopiedAlert": { "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" }, - "aliasDomain": { - "message": "Alias domain" + "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { + "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "logInWithPasskey": { + "message": "Log in with passkey?" }, - "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "savePasskeyInBitwarden": { + "message": "Save passkey in Bitwarden?" }, - "turnOffMasterPasswordPromptToEditField": { - "message": "Turn off master password re-prompt to edit this field", - "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." + "passkeyAlreadyExists": { + "message": "A passkey already exists for this application." + }, + "noPasskeysFoundForThisApplication": { + "message": "No passkeys found for this application." + }, + "noMatchingPasskeyLogin": { + "message": "You do not have a matching login for this site." + }, + "confirm": { + "message": "Confirm" + }, + "savePasskey": { + "message": "Save passkey" + }, + "savePasskeyNewLogin": { + "message": "Save passkey as new login" + }, + "choosePasskey": { + "message": "Choose a login to save this passkey to" + }, + "fido2Item": { + "message": "Fido2 Item" + }, + "overwritePasskey": { + "message": "Overwrite passkey?" + }, + "overwritePasskeyAlert": { + "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + }, + "featureNotSupported": { + "message": "Feature not yet supported" + }, + "searchLogins": { + "message": "Search all logins" + }, + "yourPasskeyIsLocked": { + "message": "Authentication required to use passkey. Verify your identity to continue." + }, + "loginToSavePasskey": { + "message": "Log in to use passkeys in Bitwarden" + }, + "useBrowserName": { + "message": "Use browser" } } diff --git a/apps/browser/src/auth/guards/fido2-auth.guard.ts b/apps/browser/src/auth/guards/fido2-auth.guard.ts index a5e08871b6a..dfe70c6f0b3 100644 --- a/apps/browser/src/auth/guards/fido2-auth.guard.ts +++ b/apps/browser/src/auth/guards/fido2-auth.guard.ts @@ -21,11 +21,6 @@ export const fido2AuthGuard: CanActivateFn = async ( const authStatus = await authService.getAuthStatus(); - if (authStatus === AuthenticationStatus.LoggedOut) { - routerService.setPreviousUrl(state.url); - return router.createUrlTree(["/home"], { queryParams: route.queryParams }); - } - if (authStatus === AuthenticationStatus.Locked) { routerService.setPreviousUrl(state.url); return router.createUrlTree(["/lock"], { queryParams: route.queryParams }); diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html index e787e0106d1..989dc8a9537 100644 --- a/apps/browser/src/auth/popup/lock.component.html +++ b/apps/browser/src/auth/popup/lock.component.html @@ -11,81 +11,91 @@

-
-
-
-
- - -
-
- - -
-
- + +
+
+
+
+ + +
+
+ + +
+
+ +
+
- -
-
- -

- -

- - {{ biometricError }} -

- {{ "awaitDesktop" | i18n }} -

+

+ +

+ + {{ biometricError }} +

+ {{ "awaitDesktop" | i18n }} +

+ + +
diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 0e3502dc8e5..4a0c752a5d2 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -23,6 +23,7 @@ import { DialogService } from "@bitwarden/components"; import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors"; import { BrowserRouterService } from "../../platform/popup/services/browser-router.service"; +import { fido2PopoutSessionData$ } from "../../vault/fido2/browser-fido2-user-interface.service"; @Component({ selector: "app-lock", @@ -33,6 +34,7 @@ export class LockComponent extends BaseLockComponent { biometricError: string; pendingBiometric = false; + fido2PopoutSessionData$ = fido2PopoutSessionData$(); constructor( router: Router, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 01d58622e3a..582c0782caf 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -560,8 +560,9 @@ export default class MainBackground { this.browserPopoutWindowService = new BrowserPopoutWindowService(); - this.popupUtilsService = new PopupUtilsService(this.isPrivateMode); - this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.popupUtilsService); + this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService( + this.browserPopoutWindowService + ); this.fido2AuthenticatorService = new Fido2AuthenticatorService( this.cipherService, this.fido2UserInterfaceService, @@ -571,6 +572,7 @@ export default class MainBackground { this.fido2ClientService = new Fido2ClientService( this.fido2AuthenticatorService, this.configService, + this.authService, this.logService ); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index d169e63e9d1..3a6ed213d80 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -258,11 +258,11 @@ export default class RuntimeBackground { return await this.main.fido2ClientService.isFido2FeatureEnabled(); case "fido2RegisterCredentialRequest": return await this.abortManager.runWithAbortController(msg.requestId, (abortController) => - this.main.fido2ClientService.createCredential(msg.data, abortController) + this.main.fido2ClientService.createCredential(msg.data, sender.tab, abortController) ); case "fido2GetCredentialRequest": return await this.abortManager.runWithAbortController(msg.requestId, (abortController) => - this.main.fido2ClientService.assertCredential(msg.data, abortController) + this.main.fido2ClientService.assertCredential(msg.data, sender.tab, abortController) ); } } diff --git a/apps/browser/src/platform/popup/abstractions/browser-popout-window.service.ts b/apps/browser/src/platform/popup/abstractions/browser-popout-window.service.ts index 0b3f55ee990..e25bed10677 100644 --- a/apps/browser/src/platform/popup/abstractions/browser-popout-window.service.ts +++ b/apps/browser/src/platform/popup/abstractions/browser-popout-window.service.ts @@ -10,6 +10,15 @@ interface BrowserPopoutWindowService { } ): Promise; closePasswordRepromptPrompt(): Promise; + openFido2Popout( + senderWindowId: number, + promptData: { + sessionId: string; + senderTabId: number; + fallbackSupported: boolean; + } + ): Promise; + closeFido2Popout(): Promise; } export { BrowserPopoutWindowService }; diff --git a/apps/browser/src/platform/popup/browser-popout-window.service.ts b/apps/browser/src/platform/popup/browser-popout-window.service.ts index ee03e3a2ec4..96c3d8b9c46 100644 --- a/apps/browser/src/platform/popup/browser-popout-window.service.ts +++ b/apps/browser/src/platform/popup/browser-popout-window.service.ts @@ -49,29 +49,65 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface { await this.closeSingleActionPopout("passwordReprompt"); } + async openFido2Popout( + senderWindowId: number, + { + sessionId, + senderTabId, + fallbackSupported, + }: { + sessionId: string; + senderTabId: number; + fallbackSupported: boolean; + } + ): Promise { + await this.closeFido2Popout(); + + const promptWindowPath = + "popup/index.html#/fido2" + + "?uilocation=popout" + + `&sessionId=${sessionId}` + + `&fallbackSupported=${fallbackSupported}` + + `&senderTabId=${senderTabId}`; + + return await this.openSingleActionPopout(senderWindowId, promptWindowPath, "fido2Popout", { + width: 200, + height: 500, + }); + } + + async closeFido2Popout(): Promise { + await this.closeSingleActionPopout("fido2Popout"); + } + private async openSingleActionPopout( senderWindowId: number, popupWindowURL: string, - singleActionPopoutKey: string - ) { + singleActionPopoutKey: string, + options: chrome.windows.CreateData = {} + ): Promise { const senderWindow = senderWindowId && (await BrowserApi.getWindow(senderWindowId)); const url = chrome.extension.getURL(popupWindowURL); const offsetRight = 15; const offsetTop = 90; - const popupWidth = this.defaultPopoutWindowOptions.width; + /// Use overrides in `options` if provided, otherwise use default + const popupWidth = options?.width || this.defaultPopoutWindowOptions.width; const windowOptions = senderWindow ? { ...this.defaultPopoutWindowOptions, - url, left: senderWindow.left + senderWindow.width - popupWidth - offsetRight, top: senderWindow.top + offsetTop, + ...options, + url, } - : { ...this.defaultPopoutWindowOptions, url }; + : { ...this.defaultPopoutWindowOptions, url, ...options }; const popupWindow = await BrowserApi.createWindow(windowOptions); await this.closeSingleActionPopout(singleActionPopoutKey); this.singleActionPopoutTabIds[singleActionPopoutKey] = popupWindow?.tabs[0].id; + + return popupWindow.id; } private async closeSingleActionPopout(popoutKey: string) { diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index adc7a5c0afe..5f20ca53521 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -115,7 +115,7 @@ const routes: Routes = [ path: "2fa-options", component: TwoFactorOptionsComponent, canActivate: [UnauthGuard], - data: { state: "2fa-options" }, + data: { state: "2fa-options", doNotSaveUrl: true }, }, { path: "login-initiated", diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index b666f55fc9d..23d033473c6 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -39,6 +39,8 @@ import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { ExportComponent } from "../tools/popup/settings/export.component"; import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component"; import { CipherRowComponent } from "../vault/popup/components/cipher-row.component"; +import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component"; +import { Fido2UseBrowserLinkComponent } from "../vault/popup/components/fido2/fido2-use-browser-link.component"; import { Fido2Component } from "../vault/popup/components/fido2/fido2.component"; import { PasswordRepromptComponent } from "../vault/popup/components/password-reprompt.component"; import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component"; @@ -113,6 +115,8 @@ import "../platform/popup/locales"; EnvironmentComponent, ExcludedDomainsComponent, ExportComponent, + Fido2CipherRowComponent, + Fido2UseBrowserLinkComponent, FolderAddEditComponent, FoldersComponent, VaultFilterComponent, diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 652e7917ea4..3b401d356f5 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -621,29 +621,6 @@ main { } } -app-fido2 { - .auth-wrapper { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - padding: 0 25px; - - .btn { - margin-top: 25px; - } - } - - .box.list { - overflow-y: auto; - } -} - .login-with-device { .fingerprint-phrase-header { padding-top: 1rem; diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index e104570e563..3d9134364c7 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -153,6 +153,14 @@ body.body-full { margin: 15px 0 15px 0; } +.useBrowserlink { + padding: 0 10px 5px 10px; + position: fixed; + bottom: 10px; + left: 0; + right: 0; +} + app-options { .box { margin: 10px 0; @@ -175,3 +183,169 @@ app-vault-attachments { } } } + +app-fido2 { + .auth-wrapper { + display: flex; + flex-direction: column; + padding: 12px 24px 12px 24px; + + .auth-header { + display: flex; + justify-content: space-between; + align-items: center; + + .left { + padding-right: 10px; + + .logo { + display: inline-flex; + align-items: center; + + i.bwi { + font-size: 35px; + margin-right: 3px; + @include themify($themes) { + color: themed("primaryColor"); + } + } + + span { + font-size: 45px; + font-weight: 300; + margin-top: -3px; + @include themify($themes) { + color: themed("primaryColor"); + } + } + } + } + + .search { + padding: 7px 10px; + width: 100%; + text-align: left; + position: relative; + display: flex; + + .bwi { + position: absolute; + top: 15px; + left: 20px; + + @include themify($themes) { + color: themed("labelColor"); + } + } + + input { + width: 100%; + margin: 0; + border: none; + padding: 5px 10px 5px 30px; + border-radius: $border-radius; + + &:focus { + border-radius: $border-radius; + outline: none; + } + + &[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; + appearance: none; + background-repeat: no-repeat; + mask-image: none; + -webkit-mask-image: none; + } + } + } + } + + .auth-flow { + display: flex; + align-items: flex-start; + flex-direction: column; + margin-top: 32px; + margin-bottom: 32px; + + .subtitle { + font-family: Open Sans; + font-size: 24px; + font-style: normal; + font-weight: 600; + line-height: 32px; + } + + .box.list { + overflow-y: auto; + } + + .box-content { + max-height: 140px; + } + + @media screen and (min-height: 501px) and (max-height: 600px) { + .box-content { + max-height: 200px; + } + } + + @media screen and (min-height: 601px) { + .box-content { + max-height: 260px; + } + } + + .box-content-row { + display: flex; + justify-content: center; + align-items: center; + margin: 0px; + padding: 0px; + margin-bottom: 12px; + + button { + min-height: 44px; + } + + .row-main { + border-radius: 6px; + padding: 5px 0px 5px 12px; + + &:focus { + @include themify($themes) { + border: 2px solid themed("headerInputBackgroundFocusColor"); + } + } + + &.row-selected { + @include themify($themes) { + outline: none; + border-left: 5px solid themed("primaryColor"); + padding: 3px 0px 3px 7px; + background-color: themed("headerBackgroundHoverColor"); + color: themed("headerColor"); + } + } + } + + .row-main-content { + display: flex; + flex-direction: column; + justify-content: center; + + .detail { + min-height: 15px; + display: block; + } + } + } + + .btn { + width: 100%; + font-size: 16px; + font-weight: 600; + } + } + } +} diff --git a/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts b/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts index 272c3d44d45..e5b8a011363 100644 --- a/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts @@ -1,9 +1,13 @@ +import { inject } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; import { BehaviorSubject, EmptyError, filter, firstValueFrom, fromEvent, + fromEventPattern, + map, merge, Observable, Subject, @@ -11,7 +15,6 @@ import { take, takeUntil, throwError, - fromEventPattern, } from "rxjs"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -24,10 +27,26 @@ import { } from "@bitwarden/common/vault/abstractions/fido2/fido2-user-interface.service.abstraction"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { Popout, PopupUtilsService } from "../../popup/services/popup-utils.service"; +import { BrowserPopoutWindowService } from "../../platform/popup/abstractions/browser-popout-window.service"; const BrowserFido2MessageName = "BrowserFido2UserInterfaceServiceMessage"; +/** + * Function to retrieve FIDO2 session data from query parameters. + * Expected to be used within components tied to routes with these query parameters. + */ +export function fido2PopoutSessionData$() { + const route = inject(ActivatedRoute); + + return route.queryParams.pipe( + map((queryParams) => ({ + isFido2Session: queryParams.sessionId != null, + sessionId: queryParams.sessionId as string, + fallbackSupported: queryParams.fallbackSupported as boolean, + })) + ); +} + export class SessionClosedError extends Error { constructor() { super("Fido2UserInterfaceSession was closed"); @@ -94,15 +113,17 @@ export type BrowserFido2Message = { sessionId: string } & ( * The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it. */ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { - constructor(private popupUtilsService: PopupUtilsService) {} + constructor(private browserPopoutWindowService: BrowserPopoutWindowService) {} async newSession( fallbackSupported: boolean, + tab: chrome.tabs.Tab, abortController?: AbortController ): Promise { return await BrowserFido2UserInterfaceSession.create( - this.popupUtilsService, + this.browserPopoutWindowService, fallbackSupported, + tab, abortController ); } @@ -110,13 +131,15 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession { static async create( - popupUtilsService: PopupUtilsService, + browserPopoutWindowService: BrowserPopoutWindowService, fallbackSupported: boolean, + tab: chrome.tabs.Tab, abortController?: AbortController ): Promise { return new BrowserFido2UserInterfaceSession( - popupUtilsService, + browserPopoutWindowService, fallbackSupported, + tab, abortController ); } @@ -125,19 +148,26 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi BrowserApi.sendMessage(BrowserFido2MessageName, msg); } + static abortPopout(sessionId: string, fallbackRequested = false) { + this.sendMessage({ + sessionId: sessionId, + type: "AbortResponse", + fallbackRequested: fallbackRequested, + }); + } + private closed = false; private messages$ = (BrowserApi.messageListener$() as Observable).pipe( filter((msg) => msg.sessionId === this.sessionId) ); - private windowClosed$: Observable; - private tabClosed$: Observable; private connected$ = new BehaviorSubject(false); + private windowClosed$: Observable; private destroy$ = new Subject(); - private popout?: Popout; private constructor( - private readonly popupUtilsService: PopupUtilsService, + private readonly browserPopoutWindowService: BrowserPopoutWindowService, private readonly fallbackSupported: boolean, + private readonly tab: chrome.tabs.Tab, readonly abortController = new AbortController(), readonly sessionId = Utils.newGuid() ) { @@ -181,11 +211,6 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi (handler: any) => chrome.windows.onRemoved.removeListener(handler) ); - this.tabClosed$ = fromEventPattern( - (handler: any) => chrome.tabs.onRemoved.addListener(handler), - (handler: any) => chrome.tabs.onRemoved.removeListener(handler) - ); - BrowserFido2UserInterfaceSession.sendMessage({ type: "NewSessionCreatedRequest", sessionId, @@ -258,7 +283,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi } async close() { - this.popupUtilsService.closePopOut(this.popout); + await this.browserPopoutWindowService.closeFido2Popout(); this.closed = true; this.destroy$.next(); this.destroy$.complete(); @@ -299,7 +324,6 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi throw new Error("Cannot re-open closed session"); } - // create promise first to avoid race condition where the popout opens before we start listening const connectPromise = firstValueFrom( merge( this.connected$.pipe(filter((connected) => connected === true)), @@ -309,41 +333,24 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi ) ); - this.popout = await this.generatePopOut(); + const popoutId = await this.browserPopoutWindowService.openFido2Popout(this.tab.windowId, { + sessionId: this.sessionId, + senderTabId: this.tab.id, + fallbackSupported: this.fallbackSupported, + }); - if (this.popout.type === "window") { - const popoutWindow = this.popout; - this.windowClosed$ - .pipe( - filter((windowId) => popoutWindow.window.id === windowId), - takeUntil(this.destroy$) - ) - .subscribe(() => { - this.close(); - this.abort(); - }); - } else if (this.popout.type === "tab") { - const popoutTab = this.popout; - this.tabClosed$ - .pipe( - filter((tabId) => popoutTab.tab.id === tabId), - takeUntil(this.destroy$) - ) - .subscribe(() => { - this.close(); - this.abort(); - }); - } + this.windowClosed$ + .pipe( + filter((windowId) => { + return popoutId === windowId; + }), + takeUntil(this.destroy$) + ) + .subscribe(() => { + this.close(); + this.abort(); + }); await connectPromise; } - - private async generatePopOut() { - const queryParams = new URLSearchParams({ sessionId: this.sessionId }); - return this.popupUtilsService.popOut( - null, - `popup/index.html?uilocation=popout#/fido2?${queryParams.toString()}`, - { center: true } - ); - } } diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.html b/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.html new file mode 100644 index 00000000000..42e8a6b6298 --- /dev/null +++ b/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.html @@ -0,0 +1,27 @@ +
+
+ +
+
diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts new file mode 100644 index 00000000000..21ff136bf42 --- /dev/null +++ b/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts @@ -0,0 +1,20 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +@Component({ + selector: "app-fido2-cipher-row", + templateUrl: "fido2-cipher-row.component.html", +}) +export class Fido2CipherRowComponent { + @Output() onSelected = new EventEmitter(); + @Input() cipher: CipherView; + @Input() last: boolean; + @Input() title: string; + @Input() isSearching: boolean; + @Input() isSelected: boolean; + + selectCipher(c: CipherView) { + this.onSelected.emit(c); + } +} diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.html b/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.html new file mode 100644 index 00000000000..886ec3247d9 --- /dev/null +++ b/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.html @@ -0,0 +1,5 @@ + diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts new file mode 100644 index 00000000000..712f728c320 --- /dev/null +++ b/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts @@ -0,0 +1,21 @@ +import { Component } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { + BrowserFido2UserInterfaceSession, + fido2PopoutSessionData$, +} from "../../../fido2/browser-fido2-user-interface.service"; + +@Component({ + selector: "app-fido2-use-browser-link", + templateUrl: "fido2-use-browser-link.component.html", +}) +export class Fido2UseBrowserLinkComponent { + fido2PopoutSessionData$ = fido2PopoutSessionData$(); + + async abort() { + const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); + BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId, true); + return; + } +} diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.html b/apps/browser/src/vault/popup/components/fido2/fido2.component.html index 378c67f10ac..5ce39c89c9c 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.html +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.html @@ -1,49 +1,139 @@ -
- - - Verification required by the initiating site. This feature is not yet implemented for accounts - without master password. - - +
+
+ + + + + + +
+ + +
+ +
+
+
+ + - A site is asking for authentication, please choose one of the following credentials to use: -
-
- -
+
+

+ {{ subtitleText | i18n }} +

+ + +
+
+ +
+
+ +
+ +
+
+ + +
+ +
+
- A passkey already exists in Bitwarden for this account -
-
- +
+

{{ "passkeyAlreadyExists" | i18n }}

+
+
+ +
+
- You do not have a matching login for this site. +
+

{{ "noPasskeysFoundForThisApplication" | i18n }}

+
+
- - +
diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts index 6e8302ae532..bbf7e55ad77 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { BehaviorSubject, combineLatest, @@ -12,10 +12,23 @@ import { takeUntil, } from "rxjs"; +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; +import { SecureNoteType } from "@bitwarden/common/enums"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; +import { DialogService } from "@bitwarden/components"; import { BrowserApi } from "../../../../platform/browser/browser-api"; import { @@ -25,7 +38,6 @@ import { interface ViewData { message: BrowserFido2Message; - showUnsupportedVerification: boolean; fallbackSupported: boolean; } @@ -36,89 +48,177 @@ interface ViewData { }) export class Fido2Component implements OnInit, OnDestroy { private destroy$ = new Subject(); + private hasSearched = false; + private searchTimeout: any = null; + private hasLoadedAllCiphers = false; + protected cipher: CipherView; + protected searchTypeSearch = false; + protected searchPending = false; + protected searchText: string; + protected url: string; + protected hostname: string; protected data$: Observable; 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(null); constructor( + private router: Router, private activatedRoute: ActivatedRoute, private cipherService: CipherService, - private passwordRepromptService: PasswordRepromptService + private passwordRepromptService: PasswordRepromptService, + private platformUtilsService: PlatformUtilsService, + private settingsService: SettingsService, + private searchService: SearchService, + private logService: LogService, + private dialogService: DialogService ) {} - ngOnInit(): void { - const sessionId$ = this.activatedRoute.queryParamMap.pipe( + ngOnInit() { + this.searchTypeSearch = !this.platformUtilsService.isSafari(); + + const queryParams$ = this.activatedRoute.queryParamMap.pipe( take(1), - map((queryParamMap) => queryParamMap.get("sessionId")) + map((queryParamMap) => ({ + sessionId: queryParamMap.get("sessionId"), + senderTabId: queryParamMap.get("senderTabId"), + })) ); - combineLatest([sessionId$, BrowserApi.messageListener$() as Observable]) - .pipe(takeUntil(this.destroy$)) - .subscribe(([sessionId, message]) => { - this.sessionId = sessionId; - if (message.type === "NewSessionCreatedRequest" && message.sessionId !== sessionId) { - return this.abort(false); - } + combineLatest([queryParams$, BrowserApi.messageListener$() as Observable]) + .pipe( + concatMap(async ([queryParams, message]) => { + this.sessionId = queryParams.sessionId; + this.senderTabId = queryParams.senderTabId; - if (message.sessionId !== sessionId) { - return; - } + // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. + if ( + message.type === "NewSessionCreatedRequest" && + message.sessionId !== queryParams.sessionId + ) { + this.abort(false); + return; + } - if (message.type === "AbortRequest") { - return this.abort(false); - } + // Ignore messages that don't belong to the current session. + if (message.sessionId !== queryParams.sessionId) { + return; + } + + if (message.type === "AbortRequest") { + this.abort(false); + return; + } + // Show dialog if user account does not have master password + if (!(await this.passwordRepromptService.enabled())) { + await this.dialogService.openSimpleDialog({ + title: { key: "featureNotSupported" }, + content: { key: "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "info", + }); + + this.abort(true); + return; + } + + return message; + }), + filter((message) => !!message), + takeUntil(this.destroy$) + ) + .subscribe((message) => { this.message$.next(message); }); this.data$ = this.message$.pipe( filter((message) => message != undefined), concatMap(async (message) => { - if (message.type === "ConfirmNewCredentialRequest") { - this.ciphers = (await this.cipherService.getAllDecrypted()).filter( - (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted - ); - } else if (message.type === "PickCredentialRequest") { - this.ciphers = await Promise.all( - message.cipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); - return cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher)); - }) - ); - } else if (message.type === "InformExcludedCredentialRequest") { - this.ciphers = await Promise.all( - message.existingCipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); - return cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher)); - }) - ); + switch (message.type) { + case "ConfirmNewCredentialRequest": { + const activeTabs = await BrowserApi.getActiveTabs(); + this.url = activeTabs[0].url; + const equivalentDomains = this.settingsService.getEquivalentDomains(this.url); + + this.ciphers = (await this.cipherService.getAllDecrypted()).filter( + (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted + ); + this.displayedCiphers = this.ciphers.filter((cipher) => + cipher.login.matchesUri(this.url, equivalentDomains) + ); + + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + + case "PickCredentialRequest": { + this.ciphers = await Promise.all( + message.cipherIds.map(async (cipherId) => { + const cipher = await this.cipherService.get(cipherId); + return cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); + }) + ); + this.displayedCiphers = [...this.ciphers]; + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + + case "InformExcludedCredentialRequest": { + this.ciphers = await Promise.all( + message.existingCipherIds.map(async (cipherId) => { + const cipher = await this.cipherService.get(cipherId); + return cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); + }) + ); + this.displayedCiphers = [...this.ciphers]; + + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } } + this.subtitleText = + this.displayedCiphers.length > 0 + ? this.getCredentialSubTitleText(message.type) + : "noMatchingPasskeyLogin"; + + this.credentialText = this.getCredentialButtonText(message.type); return { message, - showUnsupportedVerification: - "userVerification" in message && - message.userVerification && - !(await this.passwordRepromptService.enabled()), fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, }; }), takeUntil(this.destroy$) ); - sessionId$.pipe(takeUntil(this.destroy$)).subscribe((sessionId) => { + queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { this.send({ - sessionId: sessionId, + sessionId: queryParams.sessionId, type: "ConnectResponse", }); }); } - async pick(cipher: CipherView) { + async submit() { const data = this.message$.value; if (data?.type === "PickCredentialRequest") { let userVerified = false; @@ -128,19 +228,32 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, - cipherId: cipher.id, + cipherId: this.cipher.id, type: "PickCredentialResponse", userVerified, }); } else if (data?.type === "ConfirmNewCredentialRequest") { let userVerified = false; + + if (this.cipher.login.fido2Credentials.length > 0) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "overwritePasskey" }, + content: { key: "overwritePasskeyAlert" }, + type: "info", + }); + + if (!confirmed) { + return false; + } + } + if (data.userVerification) { userVerified = await this.passwordRepromptService.showPasswordPrompt(); } this.send({ sessionId: this.sessionId, - cipherId: cipher.id, + cipherId: this.cipher.id, type: "ConfirmNewCredentialResponse", userVerified, }); @@ -149,6 +262,131 @@ export class Fido2Component implements OnInit, OnDestroy { this.loading = true; } + async saveNewLogin() { + const data = this.message$.value; + if (data?.type === "ConfirmNewCredentialRequest") { + let userVerified = false; + if (data.userVerification) { + userVerified = await this.passwordRepromptService.showPasswordPrompt(); + } + + if (!data.userVerification || userVerified) { + await this.createNewCipher(); + } + + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher?.id, + type: "ConfirmNewCredentialResponse", + userVerified, + }); + } + + this.loading = true; + } + + getCredentialSubTitleText(messageType: string): string { + return messageType == "ConfirmNewCredentialRequest" ? "choosePasskey" : "logInWithPasskey"; + } + + getCredentialButtonText(messageType: string): string { + return messageType == "ConfirmNewCredentialRequest" ? "savePasskey" : "confirm"; + } + + selectedPasskey(item: CipherView) { + this.cipher = item; + } + + viewPasskey() { + this.router.navigate(["/view-cipher"], { + queryParams: { + cipherId: this.cipher.id, + uilocation: "popout", + senderTabId: this.senderTabId, + sessionId: this.sessionId, + }, + }); + } + + addCipher() { + this.router.navigate(["/add-cipher"], { + queryParams: { + name: Utils.getHostname(this.url), + uri: this.url, + uilocation: "popout", + senderTabId: this.senderTabId, + sessionId: this.sessionId, + }, + }); + } + + buildCipher() { + this.cipher = new CipherView(); + this.cipher.name = Utils.getHostname(this.url); + this.cipher.type = CipherType.Login; + this.cipher.login = new LoginView(); + this.cipher.login.uris = [new LoginUriView()]; + this.cipher.login.uris[0].uri = this.url; + this.cipher.card = new CardView(); + this.cipher.identity = new IdentityView(); + this.cipher.secureNote = new SecureNoteView(); + this.cipher.secureNote.type = SecureNoteType.Generic; + this.cipher.reprompt = CipherRepromptType.None; + } + + async createNewCipher() { + this.buildCipher(); + const cipher = await this.cipherService.encrypt(this.cipher); + try { + await this.cipherService.createWithServer(cipher); + this.cipher.id = cipher.id; + } catch (e) { + this.logService.error(e); + } + } + + async loadLoginCiphers() { + this.ciphers = (await this.cipherService.getAllDecrypted()).filter( + (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted + ); + if (!this.hasLoadedAllCiphers) { + this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText); + } + await this.search(null); + } + + async search(timeout: number = null) { + this.searchPending = false; + if (this.searchTimeout != null) { + clearTimeout(this.searchTimeout); + } + + if (timeout == null) { + this.hasSearched = this.searchService.isSearchable(this.searchText); + this.displayedCiphers = await this.searchService.searchCiphers( + this.searchText, + null, + this.ciphers + ); + return; + } + this.searchPending = true; + this.searchTimeout = setTimeout(async () => { + this.hasSearched = this.searchService.isSearchable(this.searchText); + if (!this.hasLoadedAllCiphers && !this.hasSearched) { + await this.loadLoginCiphers(); + } else { + this.displayedCiphers = await this.searchService.searchCiphers( + this.searchText, + null, + this.ciphers + ); + } + this.searchPending = false; + this.selectedPasskey(this.displayedCiphers[0]); + }, timeout); + } + abort(fallback: boolean) { this.unload(fallback); window.close(); diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts index 54cb08ae893..41ef6105bd1 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts @@ -1,7 +1,8 @@ import { Location } from "@angular/common"; import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { first } from "rxjs/operators"; +import { firstValueFrom } from "rxjs"; +import { first, takeUntil } from "rxjs/operators"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -24,6 +25,10 @@ import { DialogService } from "@bitwarden/components"; import { BrowserApi } from "../../../../platform/browser/browser-api"; import { PopupUtilsService } from "../../../../popup/services/popup-utils.service"; +import { + BrowserFido2UserInterfaceSession, + fido2PopoutSessionData$, +} from "../../../fido2/browser-fido2-user-interface.service"; @Component({ selector: "app-vault-add-edit", @@ -35,6 +40,11 @@ export class AddEditComponent extends BaseAddEditComponent { showAttachments = true; openAttachmentsInPopup: boolean; showAutoFillOnPageLoadOptions: boolean; + inPopout = false; + senderTabId?: number; + uilocation?: "popout" | "popup" | "sidebar" | "tab"; + + private fido2PopoutSessionData$ = fido2PopoutSessionData$(); constructor( cipherService: CipherService, @@ -79,6 +89,13 @@ export class AddEditComponent extends BaseAddEditComponent { async ngOnInit() { await super.ngOnInit(); + this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((value) => { + this.senderTabId = parseInt(value?.senderTabId, 10) || undefined; + this.uilocation = value?.uilocation; + }); + + this.inPopout = this.uilocation === "popout" || this.popupUtilsService.inPopout(window); + // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.queryParams.pipe(first()).subscribe(async (params) => { if (params.cipherId) { @@ -156,6 +173,12 @@ export class AddEditComponent extends BaseAddEditComponent { return false; } + // Would be refactored after rework is done on the windows popout service + const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); + if (this.inPopout && sessionData.isFido2Session) { + return; + } + if (this.popupUtilsService.inTab(window)) { this.popupUtilsService.disableCloseTabWarning(); this.messagingService.send("closeTab", { delay: 1000 }); @@ -191,17 +214,37 @@ export class AddEditComponent extends BaseAddEditComponent { } } - cancel() { + async cancel() { super.cancel(); + // Would be refactored after rework is done on the windows popout service + const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); + if (this.inPopout && sessionData.isFido2Session) { + BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId); + return; + } + if (this.popupUtilsService.inTab(window)) { this.messagingService.send("closeTab"); return; } + if (this.inPopout && this.senderTabId) { + this.close(); + return; + } + this.location.back(); } + // Used for closing single-action views + close() { + BrowserApi.focusTab(this.senderTabId); + window.close(); + + return; + } + async generateUsername(): Promise { const confirmed = await super.generateUsername(); if (confirmed) { diff --git a/apps/browser/src/vault/popup/components/vault/view.component.ts b/apps/browser/src/vault/popup/components/vault/view.component.ts index 73381dce4d0..eabd9442eb0 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view.component.ts @@ -1,7 +1,7 @@ import { Location } from "@angular/common"; import { ChangeDetectorRef, Component, NgZone } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { first } from "rxjs/operators"; import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component"; @@ -28,6 +28,10 @@ import { DialogService } from "@bitwarden/components"; import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service"; import { BrowserApi } from "../../../../platform/browser/browser-api"; import { PopupUtilsService } from "../../../../popup/services/popup-utils.service"; +import { + BrowserFido2UserInterfaceSession, + fido2PopoutSessionData$, +} from "../../../fido2/browser-fido2-user-interface.service"; const BroadcasterSubscriptionId = "ChildViewComponent"; @@ -55,6 +59,7 @@ export class ViewComponent extends BaseViewComponent { uilocation?: "popout" | "popup" | "sidebar" | "tab"; loadPageDetailsTimeout: number; inPopout = false; + private fido2PopoutSessionData$ = fido2PopoutSessionData$(); private destroy$ = new Subject(); @@ -299,7 +304,14 @@ export class ViewComponent extends BaseViewComponent { return false; } - close() { + async close() { + // Would be refactored after rework is done on the windows popout service + const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); + if (this.inPopout && sessionData.isFido2Session) { + BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId); + return; + } + if (this.inPopout && this.senderTabId) { BrowserApi.focusTab(this.senderTabId); window.close(); diff --git a/libs/common/src/vault/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/vault/abstractions/fido2/fido2-authenticator.service.abstraction.ts index 04ec7123afd..5a406aeb14c 100644 --- a/libs/common/src/vault/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -15,6 +15,7 @@ export abstract class Fido2AuthenticatorService { **/ makeCredential: ( params: Fido2AuthenticatorMakeCredentialsParams, + tab: chrome.tabs.Tab, abortController?: AbortController ) => Promise; @@ -28,6 +29,7 @@ export abstract class Fido2AuthenticatorService { */ getAssertion: ( params: Fido2AuthenticatorGetAssertionParams, + tab: chrome.tabs.Tab, abortController?: AbortController ) => Promise; } diff --git a/libs/common/src/vault/abstractions/fido2/fido2-client.service.abstraction.ts b/libs/common/src/vault/abstractions/fido2/fido2-client.service.abstraction.ts index a87168e3ebc..fca73c8d99e 100644 --- a/libs/common/src/vault/abstractions/fido2/fido2-client.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/fido2/fido2-client.service.abstraction.ts @@ -24,6 +24,7 @@ export abstract class Fido2ClientService { */ createCredential: ( params: CreateCredentialParams, + tab: chrome.tabs.Tab, abortController?: AbortController ) => Promise; @@ -38,6 +39,7 @@ export abstract class Fido2ClientService { */ assertCredential: ( params: AssertCredentialParams, + tab: chrome.tabs.Tab, abortController?: AbortController ) => Promise; diff --git a/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts index f1e18b4a3a7..fe15aec0fdc 100644 --- a/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -50,6 +50,7 @@ export abstract class Fido2UserInterfaceService { */ newSession: ( fallbackSupported: boolean, + tab: chrome.tabs.Tab, abortController?: AbortController ) => Promise; } diff --git a/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts index 93c0becaa21..b974c1793e4 100644 --- a/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts +++ b/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts @@ -34,6 +34,7 @@ describe("FidoAuthenticatorService", () => { let userInterfaceSession!: MockProxy; let syncService!: MockProxy; let authenticator!: Fido2AuthenticatorService; + let tab!: chrome.tabs.Tab; beforeEach(async () => { cipherService = mock(); @@ -42,6 +43,7 @@ describe("FidoAuthenticatorService", () => { userInterface.newSession.mockResolvedValue(userInterfaceSession); syncService = mock(); authenticator = new Fido2AuthenticatorService(cipherService, userInterface, syncService); + tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; }); describe("makeCredential", () => { @@ -55,19 +57,19 @@ describe("FidoAuthenticatorService", () => { // Spec: Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation. it("should throw error when input does not contain any supported algorithms", async () => { const result = async () => - await authenticator.makeCredential(invalidParams.unsupportedAlgorithm); + await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotSupported); }); it("should throw error when requireResidentKey has invalid value", async () => { - const result = async () => await authenticator.makeCredential(invalidParams.invalidRk); + const result = async () => await authenticator.makeCredential(invalidParams.invalidRk, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown); }); it("should throw error when requireUserVerification has invalid value", async () => { - const result = async () => await authenticator.makeCredential(invalidParams.invalidUv); + const result = async () => await authenticator.makeCredential(invalidParams.invalidUv, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown); }); @@ -80,7 +82,7 @@ describe("FidoAuthenticatorService", () => { it.skip("should throw error if requireUserVerification is set to true", async () => { const params = await createParams({ requireUserVerification: true }); - const result = async () => await authenticator.makeCredential(params); + const result = async () => await authenticator.makeCredential(params, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Constraint); }); @@ -94,7 +96,7 @@ describe("FidoAuthenticatorService", () => { for (const p of Object.values(invalidParams)) { try { - await authenticator.makeCredential(p); + await authenticator.makeCredential(p, tab); // eslint-disable-next-line no-empty } catch {} } @@ -135,7 +137,7 @@ describe("FidoAuthenticatorService", () => { userInterfaceSession.informExcludedCredential.mockResolvedValue(); try { - await authenticator.makeCredential(params); + await authenticator.makeCredential(params, tab); // eslint-disable-next-line no-empty } catch {} @@ -146,7 +148,7 @@ describe("FidoAuthenticatorService", () => { it("should throw error", async () => { userInterfaceSession.informExcludedCredential.mockResolvedValue(); - const result = async () => await authenticator.makeCredential(params); + const result = async () => await authenticator.makeCredential(params, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed); }); @@ -157,7 +159,7 @@ describe("FidoAuthenticatorService", () => { excludedCipher.organizationId = "someOrganizationId"; try { - await authenticator.makeCredential(params); + await authenticator.makeCredential(params, tab); // eslint-disable-next-line no-empty } catch {} @@ -170,7 +172,7 @@ describe("FidoAuthenticatorService", () => { for (const p of Object.values(invalidParams)) { try { - await authenticator.makeCredential(p); + await authenticator.makeCredential(p, tab); // eslint-disable-next-line no-empty } catch {} } @@ -207,7 +209,7 @@ describe("FidoAuthenticatorService", () => { userVerified: userVerification, }); - await authenticator.makeCredential(params); + await authenticator.makeCredential(params, tab); expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({ credentialName: params.rpEntity.name, @@ -225,7 +227,7 @@ describe("FidoAuthenticatorService", () => { }); cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); - await authenticator.makeCredential(params); + await authenticator.makeCredential(params, tab); const saved = cipherService.encrypt.mock.lastCall?.[0]; expect(saved).toEqual( @@ -262,7 +264,7 @@ describe("FidoAuthenticatorService", () => { }); const params = await createParams(); - const result = async () => await authenticator.makeCredential(params); + const result = async () => await authenticator.makeCredential(params, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed); }); @@ -277,7 +279,7 @@ describe("FidoAuthenticatorService", () => { cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); - const result = async () => await authenticator.makeCredential(params); + const result = async () => await authenticator.makeCredential(params, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown); }); @@ -318,7 +320,7 @@ describe("FidoAuthenticatorService", () => { }); it("should return attestation object", async () => { - const result = await authenticator.makeCredential(params); + const result = await authenticator.makeCredential(params, tab); const attestationObject = CBOR.decode( Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer @@ -415,7 +417,7 @@ describe("FidoAuthenticatorService", () => { describe("invalid input parameters", () => { it("should throw error when requireUserVerification has invalid value", async () => { - const result = async () => await authenticator.getAssertion(invalidParams.invalidUv); + const result = async () => await authenticator.getAssertion(invalidParams.invalidUv, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown); }); @@ -428,7 +430,7 @@ describe("FidoAuthenticatorService", () => { it.skip("should throw error if requireUserVerification is set to true", async () => { const params = await createParams({ requireUserVerification: true }); - const result = async () => await authenticator.getAssertion(params); + const result = async () => await authenticator.getAssertion(params, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Constraint); }); @@ -457,7 +459,7 @@ describe("FidoAuthenticatorService", () => { userInterfaceSession.informCredentialNotFound.mockResolvedValue(); try { - await authenticator.getAssertion(params); + await authenticator.getAssertion(params, tab); // eslint-disable-next-line no-empty } catch {} @@ -472,7 +474,7 @@ describe("FidoAuthenticatorService", () => { userInterfaceSession.informCredentialNotFound.mockResolvedValue(); try { - await authenticator.getAssertion(params); + await authenticator.getAssertion(params, tab); // eslint-disable-next-line no-empty } catch {} @@ -493,7 +495,7 @@ describe("FidoAuthenticatorService", () => { /** Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. */ it("should throw error", async () => { - const result = async () => await authenticator.getAssertion(params); + const result = async () => await authenticator.getAssertion(params, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed); }); @@ -532,7 +534,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - await authenticator.getAssertion(params); + await authenticator.getAssertion(params, tab); expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ cipherIds: ciphers.map((c) => c.id), @@ -548,7 +550,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - await authenticator.getAssertion(params); + await authenticator.getAssertion(params, tab); expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ cipherIds: [discoverableCiphers[0].id], @@ -565,7 +567,7 @@ describe("FidoAuthenticatorService", () => { userVerified: userVerification, }); - await authenticator.getAssertion(params); + await authenticator.getAssertion(params, tab); expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ cipherIds: ciphers.map((c) => c.id), @@ -581,7 +583,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - const result = async () => await authenticator.getAssertion(params); + const result = async () => await authenticator.getAssertion(params, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed); }); @@ -629,7 +631,7 @@ describe("FidoAuthenticatorService", () => { const encrypted = Symbol(); cipherService.encrypt.mockResolvedValue(encrypted as any); - await authenticator.getAssertion(params); + await authenticator.getAssertion(params, tab); expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted); @@ -648,7 +650,7 @@ describe("FidoAuthenticatorService", () => { }); it("should return an assertion result", async () => { - const result = await authenticator.getAssertion(params); + const result = await authenticator.getAssertion(params, tab); const encAuthData = result.authenticatorData; const rpIdHash = encAuthData.slice(0, 32); @@ -689,7 +691,7 @@ describe("FidoAuthenticatorService", () => { for (let i = 0; i < 10; ++i) { await init(); // Reset inputs - const result = await authenticator.getAssertion(params); + const result = await authenticator.getAssertion(params, tab); const counter = result.authenticatorData.slice(33, 37); expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change @@ -706,7 +708,7 @@ describe("FidoAuthenticatorService", () => { it("should throw unkown error if creation fails", async () => { cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); - const result = async () => await authenticator.getAssertion(params); + const result = async () => await authenticator.getAssertion(params, tab); await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown); }); diff --git a/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts b/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts index 801f7622f9d..b3ab710db7e 100644 --- a/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts @@ -46,10 +46,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr async makeCredential( params: Fido2AuthenticatorMakeCredentialsParams, + tab: chrome.tabs.Tab, abortController?: AbortController ): Promise { const userInterfaceSession = await this.userInterface.newSession( params.fallbackSupported, + tab, abortController ); @@ -175,10 +177,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr async getAssertion( params: Fido2AuthenticatorGetAssertionParams, + tab: chrome.tabs.Tab, abortController?: AbortController ): Promise { const userInterfaceSession = await this.userInterface.newSession( params.fallbackSupported, + tab, abortController ); try { diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts b/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts index 2b820fea865..597853a7b88 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction"; import { Utils } from "../../../platform/misc/utils"; import { @@ -24,13 +26,18 @@ const RpId = "bitwarden.com"; describe("FidoAuthenticatorService", () => { let authenticator!: MockProxy; let configService!: MockProxy; + let authService!: MockProxy; let client!: Fido2ClientService; + let tab!: chrome.tabs.Tab; beforeEach(async () => { authenticator = mock(); configService = mock(); - client = new Fido2ClientService(authenticator, configService); + authService = mock(); + + client = new Fido2ClientService(authenticator, configService, authService); configService.getFeatureFlag.mockResolvedValue(true); + tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; }); describe("createCredential", () => { @@ -39,7 +46,7 @@ describe("FidoAuthenticatorService", () => { it("should throw error if sameOriginWithAncestors is false", async () => { const params = createParams({ sameOriginWithAncestors: false }); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); @@ -50,7 +57,7 @@ describe("FidoAuthenticatorService", () => { it("should throw error if user.id is too small", async () => { const params = createParams({ user: { id: "", displayName: "name" } }); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); await expect(result).rejects.toBeInstanceOf(TypeError); }); @@ -64,7 +71,7 @@ describe("FidoAuthenticatorService", () => { }, }); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); await expect(result).rejects.toBeInstanceOf(TypeError); }); @@ -79,7 +86,7 @@ describe("FidoAuthenticatorService", () => { origin: "invalid-domain-name", }); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -93,7 +100,7 @@ describe("FidoAuthenticatorService", () => { rp: { id: "bitwarden.com", name: "Bitwraden" }, }); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -106,7 +113,7 @@ describe("FidoAuthenticatorService", () => { rp: { id: "bitwarden.com", name: "Bitwraden" }, }); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -122,7 +129,7 @@ describe("FidoAuthenticatorService", () => { ], }); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotSupportedError" }); @@ -137,7 +144,7 @@ describe("FidoAuthenticatorService", () => { const abortController = new AbortController(); abortController.abort(); - const result = async () => await client.createCredential(params, abortController); + const result = async () => await client.createCredential(params, tab, abortController); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "AbortError" }); @@ -152,7 +159,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - await client.createCredential(params); + await client.createCredential(params, tab); expect(authenticator.makeCredential).toHaveBeenCalledWith( expect.objectContaining({ @@ -165,6 +172,7 @@ describe("FidoAuthenticatorService", () => { displayName: params.user.displayName, }), }), + tab, expect.anything() ); }); @@ -176,7 +184,7 @@ describe("FidoAuthenticatorService", () => { new Fido2AutenticatorError(Fido2AutenticatorErrorCode.InvalidState) ); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "InvalidStateError" }); @@ -188,7 +196,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authenticator.makeCredential.mockRejectedValue(new Error("unknown error")); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); @@ -199,7 +207,17 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); configService.getFeatureFlag.mockResolvedValue(false); - const result = async () => await client.createCredential(params); + const result = async () => await client.createCredential(params, tab); + + const rejects = expect(result).rejects; + await rejects.toThrow(FallbackRequestedError); + }); + + it("should throw FallbackRequestedError if user is logged out", async () => { + const params = createParams(); + authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut); + + const result = async () => await client.createCredential(params, tab); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -256,7 +274,7 @@ describe("FidoAuthenticatorService", () => { origin: "invalid-domain-name", }); - const result = async () => await client.assertCredential(params); + const result = async () => await client.assertCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -270,7 +288,7 @@ describe("FidoAuthenticatorService", () => { rpId: "bitwarden.com", }); - const result = async () => await client.assertCredential(params); + const result = async () => await client.assertCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -283,7 +301,7 @@ describe("FidoAuthenticatorService", () => { rpId: "bitwarden.com", }); - const result = async () => await client.assertCredential(params); + const result = async () => await client.assertCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -298,7 +316,7 @@ describe("FidoAuthenticatorService", () => { const abortController = new AbortController(); abortController.abort(); - const result = async () => await client.assertCredential(params, abortController); + const result = async () => await client.assertCredential(params, tab, abortController); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "AbortError" }); @@ -314,7 +332,7 @@ describe("FidoAuthenticatorService", () => { new Fido2AutenticatorError(Fido2AutenticatorErrorCode.InvalidState) ); - const result = async () => await client.assertCredential(params); + const result = async () => await client.assertCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "InvalidStateError" }); @@ -326,7 +344,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authenticator.getAssertion.mockRejectedValue(new Error("unknown error")); - const result = async () => await client.assertCredential(params); + const result = async () => await client.assertCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); @@ -337,7 +355,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); configService.getFeatureFlag.mockResolvedValue(false); - const result = async () => await client.assertCredential(params); + const result = async () => await client.assertCredential(params, tab); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -348,12 +366,22 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); params.sameOriginWithAncestors = false; // Simulating the falsey value - const result = async () => await client.assertCredential(params); + const result = async () => await client.assertCredential(params, tab); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); await rejects.toBeInstanceOf(DOMException); }); + + it("should throw FallbackRequestedError if user is logged out", async () => { + const params = createParams(); + authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut); + + const result = async () => await client.assertCredential(params, tab); + + const rejects = expect(result).rejects; + await rejects.toThrow(FallbackRequestedError); + }); }); describe("assert non-discoverable credential", () => { @@ -369,7 +397,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); - await client.assertCredential(params); + await client.assertCredential(params, tab); expect(authenticator.getAssertion).toHaveBeenCalledWith( expect.objectContaining({ @@ -387,6 +415,7 @@ describe("FidoAuthenticatorService", () => { }), ], }), + tab, expect.anything() ); }); @@ -400,7 +429,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); - await client.assertCredential(params); + await client.assertCredential(params, tab); expect(authenticator.getAssertion).toHaveBeenCalledWith( expect.objectContaining({ @@ -408,6 +437,7 @@ describe("FidoAuthenticatorService", () => { rpId: RpId, allowCredentialDescriptorList: [], }), + tab, expect.anything() ); }); diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.ts b/libs/common/src/vault/services/fido2/fido2-client.service.ts index 1b4c2cbf9ef..8733fb21cb5 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.ts @@ -1,5 +1,7 @@ import { parse } from "tldts"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { FeatureFlag } from "../../../enums/feature-flag.enum"; import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction"; import { LogService } from "../../../platform/abstractions/log.service"; @@ -37,6 +39,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { constructor( private authenticator: Fido2AuthenticatorService, private configService: ConfigServiceAbstraction, + private authService: AuthService, private logService?: LogService ) {} @@ -46,6 +49,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { async createCredential( params: CreateCredentialParams, + tab: chrome.tabs.Tab, abortController = new AbortController() ): Promise { const enableFido2VaultCredentials = await this.isFido2FeatureEnabled(); @@ -55,6 +59,13 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { throw new FallbackRequestedError(); } + const authStatus = await this.authService.getAuthStatus(); + + if (authStatus === AuthenticationStatus.LoggedOut) { + this.logService?.warning(`[Fido2Client] Fido2VaultCredential is not enabled`); + throw new FallbackRequestedError(); + } + if (!params.sameOriginWithAncestors) { this.logService?.warning( `[Fido2Client] Invalid 'sameOriginWithAncestors' value: ${params.sameOriginWithAncestors}` @@ -126,7 +137,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { // Set timeout before invoking authenticator if (abortController.signal.aborted) { this.logService?.info(`[Fido2Client] Aborted with AbortController`); - throw new DOMException(undefined, "AbortError"); + throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } const timeout = setAbortTimeout( abortController, @@ -138,6 +149,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { try { makeCredentialResult = await this.authenticator.makeCredential( makeCredentialParams, + tab, abortController ); } catch (error) { @@ -154,16 +166,19 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { error.errorCode === Fido2AutenticatorErrorCode.InvalidState ) { this.logService?.warning(`[Fido2Client] Unknown error: ${error}`); - throw new DOMException(undefined, "InvalidStateError"); + throw new DOMException("Unknown error occured.", "InvalidStateError"); } this.logService?.info(`[Fido2Client] Aborted by user: ${error}`); - throw new DOMException(undefined, "NotAllowedError"); + throw new DOMException( + "The operation either timed out or was not allowed.", + "NotAllowedError" + ); } if (abortController.signal.aborted) { this.logService?.info(`[Fido2Client] Aborted with AbortController`); - throw new DOMException(undefined, "AbortError"); + throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } clearTimeout(timeout); @@ -179,6 +194,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { async assertCredential( params: AssertCredentialParams, + tab: chrome.tabs.Tab, abortController = new AbortController() ): Promise { const enableFido2VaultCredentials = await this.isFido2FeatureEnabled(); @@ -188,6 +204,13 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { throw new FallbackRequestedError(); } + const authStatus = await this.authService.getAuthStatus(); + + if (authStatus === AuthenticationStatus.LoggedOut) { + this.logService?.warning(`[Fido2Client] Fido2VaultCredential is not enabled`); + throw new FallbackRequestedError(); + } + if (!params.sameOriginWithAncestors) { this.logService?.warning( `[Fido2Client] Invalid 'sameOriginWithAncestors' value: ${params.sameOriginWithAncestors}` @@ -230,7 +253,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { if (abortController.signal.aborted) { this.logService?.info(`[Fido2Client] Aborted with AbortController`); - throw new DOMException(undefined, "AbortError"); + throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } const timeout = setAbortTimeout(abortController, params.userVerification, params.timeout); @@ -239,6 +262,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { try { getAssertionResult = await this.authenticator.getAssertion( getAssertionParams, + tab, abortController ); } catch (error) { @@ -255,16 +279,19 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { error.errorCode === Fido2AutenticatorErrorCode.InvalidState ) { this.logService?.warning(`[Fido2Client] Unknown error: ${error}`); - throw new DOMException(undefined, "InvalidStateError"); + throw new DOMException("Unknown error occured.", "InvalidStateError"); } this.logService?.info(`[Fido2Client] Aborted by user: ${error}`); - throw new DOMException(undefined, "NotAllowedError"); + throw new DOMException( + "The operation either timed out or was not allowed.", + "NotAllowedError" + ); } if (abortController.signal.aborted) { this.logService?.info(`[Fido2Client] Aborted with AbortController`); - throw new DOMException(undefined, "AbortError"); + throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } clearTimeout(timeout);