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 3a8c69cba9..c0389f5afd 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts @@ -18,7 +18,7 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai 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 { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; +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"; @@ -311,6 +311,7 @@ export class Fido2Component implements OnInit, OnDestroy { queryParams: { name: data.credentialName || data.rpId, uri: this.url, + type: CipherType.Login.toString(), uilocation: "popout", username: data.userName, senderTabId: this.senderTabId, diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index 0ae2f0af01..863b5e8dc3 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -10,7 +10,8 @@ *ngIf="!loading" formId="cipherForm" [config]="config" - (cipherSaved)="onCipherSaved()" + (cipherSaved)="onCipherSaved($event)" + [beforeSubmit]="checkFido2UserVerification" [submitBtn]="submitBtn" > >; @@ -99,7 +120,7 @@ export type AddEditQueryParams = Partial>; AsyncActionsModule, ], }) -export class AddEditV2Component { +export class AddEditV2Component implements OnInit { headerText: string; config: CipherFormConfig; @@ -111,16 +132,50 @@ export class AddEditV2Component { return this.config?.originalCipher?.id as CipherId; } + private fido2PopoutSessionData$ = fido2PopoutSessionData$(); + private fido2PopoutSessionData: Fido2SessionData; + + private get inFido2PopoutWindow() { + return BrowserPopupUtils.inPopout(window) && this.fido2PopoutSessionData.isFido2Session; + } + + private get inSingleActionPopout() { + return BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.addEditVaultItem); + } + constructor( private route: ActivatedRoute, private location: Location, private i18nService: I18nService, private addEditFormConfigService: CipherFormConfigService, private router: Router, + private popupCloseWarningService: PopupCloseWarningService, ) { this.subscribeToParams(); } + async ngOnInit() { + this.fido2PopoutSessionData = await firstValueFrom(this.fido2PopoutSessionData$); + + if (BrowserPopupUtils.inPopout(window)) { + this.popupCloseWarningService.enable(); + } + } + + /** + * Called before the form is submitted, allowing us to handle Fido2 user verification. + */ + protected checkFido2UserVerification: () => Promise = async () => { + if (!this.inFido2PopoutWindow) { + // Not in a Fido2 popout window, no need to handle user verification. + return true; + } + + // TODO use fido2 user verification service once user verification for passkeys is approved for production. + // We are bypassing user verification pending approval for production. + return true; + }; + /** * Navigates to previous view or view-cipher path * depending on the history length. @@ -129,6 +184,17 @@ export class AddEditV2Component { * forced into a popout window. */ async handleBackButton() { + if (this.inFido2PopoutWindow) { + this.popupCloseWarningService.disable(); + BrowserFido2UserInterfaceSession.abortPopout(this.fido2PopoutSessionData.sessionId); + return; + } + + if (this.inSingleActionPopout) { + await BrowserPopupUtils.closeSingleActionPopout(VaultPopoutType.addEditVaultItem); + return; + } + if (history.length === 1) { await this.router.navigate(["/view-cipher"], { queryParams: { cipherId: this.originalCipherId }, @@ -138,7 +204,25 @@ export class AddEditV2Component { } } - onCipherSaved() { + async onCipherSaved(cipher: CipherView) { + if (BrowserPopupUtils.inPopout(window)) { + this.popupCloseWarningService.disable(); + } + + if (this.inFido2PopoutWindow) { + BrowserFido2UserInterfaceSession.confirmNewCredentialResponse( + this.fido2PopoutSessionData.sessionId, + cipher.id, + this.fido2PopoutSessionData.userVerification, + ); + return; + } + + if (this.inSingleActionPopout) { + await BrowserPopupUtils.closeSingleActionPopout(VaultPopoutType.addEditVaultItem, 1000); + return; + } + this.location.back(); } @@ -189,6 +273,12 @@ export class AddEditV2Component { if (params.uri) { config.initialValues.loginUri = params.uri; } + if (params.username) { + config.initialValues.username = params.username; + } + if (params.name) { + config.initialValues.name = params.name; + } } setHeader(mode: CipherFormMode, type: CipherType) { 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 ee1e46abab..fb3fa75eba 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 @@ -397,6 +397,7 @@ export class AddEditComponent extends BaseAddEditComponent { } // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production. + // Be sure to make the same changes to add-edit-v2.component.ts if applicable private async handleFido2UserVerification( sessionId: string, userVerification: boolean, diff --git a/apps/browser/src/vault/popup/utils/fido2-popout-session-data.ts b/apps/browser/src/vault/popup/utils/fido2-popout-session-data.ts index a4d95ff48f..79a15294f6 100644 --- a/apps/browser/src/vault/popup/utils/fido2-popout-session-data.ts +++ b/apps/browser/src/vault/popup/utils/fido2-popout-session-data.ts @@ -2,6 +2,18 @@ import { inject } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { map } from "rxjs"; +/** + * Interface describing the data that can be passed as query params for a FIDO2 session. + */ +export interface Fido2SessionData { + isFido2Session: boolean; + sessionId: string; + fallbackSupported: boolean; + userVerification: boolean; + senderUrl: string; + fromLock: boolean; +} + /** * Function to retrieve FIDO2 session data from query parameters. * Expected to be used within components tied to routes with these query parameters. @@ -10,13 +22,16 @@ 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 === "true", - userVerification: queryParams.userVerification === "true", - senderUrl: queryParams.senderUrl as string, - fromLock: queryParams.fromLock === "true", - })), + map( + (queryParams) => + { + isFido2Session: queryParams.sessionId != null, + sessionId: queryParams.sessionId as string, + fallbackSupported: queryParams.fallbackSupported === "true", + userVerification: queryParams.userVerification === "true", + senderUrl: queryParams.senderUrl as string, + fromLock: queryParams.fromLock === "true", + }, + ), ); } diff --git a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts index 334fbae590..837c9d11ed 100644 --- a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts @@ -22,6 +22,8 @@ export type OptionalInitialValues = { organizationId?: OrganizationId; collectionIds?: CollectionId[]; loginUri?: string; + username?: string; + name?: string; }; /** diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index b666e6833b..d7f96d19c5 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -90,6 +90,12 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci @Input() submitBtn?: ButtonComponent; + /** + * Optional function to call before submitting the form. If the function returns false, the form will not be submitted. + */ + @Input() + beforeSubmit: () => Promise; + /** * Event emitted when the cipher is saved successfully. */ @@ -213,7 +219,17 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci return; } - await this.addEditFormService.saveCipher(this.updatedCipherView, this.config); + if (this.beforeSubmit) { + const shouldSubmit = await this.beforeSubmit(); + if (!shouldSubmit) { + return; + } + } + + const savedCipher = await this.addEditFormService.saveCipher( + this.updatedCipherView, + this.config, + ); this.toastService.showToast({ variant: "success", @@ -225,6 +241,6 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci ), }); - this.cipherSaved.emit(this.updatedCipherView); + this.cipherSaved.emit(savedCipher); }; } diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index 9e84f8ea6c..ae712b915b 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -113,6 +113,10 @@ export class IdentitySectionComponent implements OnInit { if (this.originalCipherView && this.originalCipherView.id) { this.populateFormData(); + } else { + this.identityForm.patchValue({ + username: this.cipherFormContainer.config.initialValues?.username || "", + }); } } diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index 75cb160c92..99ecd84cd2 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -165,7 +165,7 @@ export class ItemDetailsSectionComponent implements OnInit { await this.initFromExistingCipher(); } else { this.itemDetailsForm.setValue({ - name: "", + name: this.initialValues?.name || "", organizationId: this.initialValues?.organizationId || this.defaultOwner, folderId: this.initialValues?.folderId || null, collectionIds: [], diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html index 085354d3db..09327a145f 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html @@ -67,7 +67,7 @@ bitIconButton="bwi-minus-circle" buttonType="danger" bitSuffix - *ngIf="loginDetailsForm.enabled" + *ngIf="loginDetailsForm.enabled && viewHiddenFields" [bitAction]="removePasskey" data-testid="remove-passkey-button" [appA11yTitle]="'removePasskey' | i18n" diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index 0fe4b128d3..31a0bb8ab5 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -452,6 +452,8 @@ describe("LoginDetailsSectionComponent", () => { fixture = TestBed.createComponent(LoginDetailsSectionComponent); component = fixture.componentInstance; + + jest.spyOn(component, "viewHiddenFields", "get").mockReturnValue(true); }); it("renders the passkey field when available", () => { @@ -469,7 +471,7 @@ describe("LoginDetailsSectionComponent", () => { it("renders the passkey remove button when editable", () => { fixture.detectChanges(); - expect(getRemovePasskeyBtn).not.toBeNull(); + expect(getRemovePasskeyBtn()).not.toBeNull(); }); it("does not render the passkey remove button when not editable", () => { @@ -480,6 +482,14 @@ describe("LoginDetailsSectionComponent", () => { expect(getRemovePasskeyBtn()).toBeNull(); }); + it("does not render the passkey remove button when viewHiddenFields is false", () => { + jest.spyOn(component, "viewHiddenFields", "get").mockReturnValue(false); + + fixture.detectChanges(); + + expect(getRemovePasskeyBtn()).toBeNull(); + }); + it("hides the passkey field when missing a passkey", () => { (cipherFormContainer.originalCipherView as CipherView).login.fido2Credentials = []; diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts index 61a51fadaa..57d2243820 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -144,9 +144,10 @@ export class LoginDetailsSectionComponent implements OnInit { } private async initNewCipher() { - this.loginDetailsForm.controls.password.patchValue( - await this.generationService.generateInitialPassword(), - ); + this.loginDetailsForm.patchValue({ + username: this.cipherFormContainer.config.initialValues?.username || "", + password: await this.generationService.generateInitialPassword(), + }); } captureTotp = async () => { diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index 166e1bd58d..4171f153ec 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -48,7 +48,7 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { return { mode, - cipherType, + cipherType: cipher?.type ?? cipherType ?? CipherType.Login, admin: false, allowPersonalOwnership, originalCipher: cipher,