1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-23 03:22:50 +02:00

[PM-9959] [PM-9962] Browser Refresh - Passkey Fixes (#10299)

* [PM-9959] Expose Fido2SessionData interface

* [PM-9959] Ensure cipherType is passed during passkey creation

* [PM-9959] Add beforeSubmit hook to cipherForm

* [PM-9959] Add support for Fido2 credential creation in add-edit-v2

* [PM-9959] Ensure cipherType defaults to CipherType.Login if none is available

* [PM-9959] Add support for name and username to be passed in as query params for initial form values

* [PM-9962] Hide remove passkey button when cipher has "except passwords" permissions
This commit is contained in:
Shane Melton 2024-07-29 08:13:56 -07:00 committed by GitHub
parent 00f6920a86
commit ad01a529e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 166 additions and 25 deletions

View File

@ -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,

View File

@ -10,7 +10,8 @@
*ngIf="!loading"
formId="cipherForm"
[config]="config"
(cipherSaved)="onCipherSaved()"
(cipherSaved)="onCipherSaved($event)"
[beforeSubmit]="checkFido2UserVerification"
[submitBtn]="submitBtn"
>
<app-open-attachments

View File

@ -1,14 +1,15 @@
import { CommonModule, Location } from "@angular/common";
import { Component } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { map, switchMap } from "rxjs";
import { firstValueFrom, map, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components";
import {
CipherFormConfig,
@ -19,10 +20,18 @@ import {
TotpCaptureService,
} from "@bitwarden/vault";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
import { PopupCloseWarningService } from "../../../../../popup/services/popup-close-warning.service";
import { BrowserFido2UserInterfaceSession } from "../../../../fido2/browser-fido2-user-interface.service";
import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service";
import {
fido2PopoutSessionData$,
Fido2SessionData,
} from "../../../utils/fido2-popout-session-data";
import { VaultPopoutType } from "../../../utils/vault-popout-window";
import { OpenAttachmentsComponent } from "../attachments/open-attachments/open-attachments.component";
/**
@ -31,12 +40,14 @@ import { OpenAttachmentsComponent } from "../attachments/open-attachments/open-a
class QueryParams {
constructor(params: Params) {
this.cipherId = params.cipherId;
this.type = parseInt(params.type, null);
this.type = params.type != undefined ? parseInt(params.type, null) : undefined;
this.clone = params.clone === "true";
this.folderId = params.folderId;
this.organizationId = params.organizationId;
this.collectionId = params.collectionId;
this.uri = params.uri;
this.username = params.username;
this.name = params.name;
}
/**
@ -47,7 +58,7 @@ class QueryParams {
/**
* The type of cipher to create.
*/
type: CipherType;
type?: CipherType;
/**
* Whether to clone the cipher.
@ -73,6 +84,16 @@ class QueryParams {
* Optional URI to pre-fill for login ciphers.
*/
uri?: string;
/**
* Optional username to pre-fill for login/identity ciphers.
*/
username?: string;
/**
* Optional name to pre-fill for the cipher.
*/
name?: string;
}
export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
@ -99,7 +120,7 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
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<boolean> = 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) {

View File

@ -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,

View File

@ -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) => ({
map(
(queryParams) =>
<Fido2SessionData>{
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",
})),
},
),
);
}

View File

@ -22,6 +22,8 @@ export type OptionalInitialValues = {
organizationId?: OrganizationId;
collectionIds?: CollectionId[];
loginUri?: string;
username?: string;
name?: string;
};
/**

View File

@ -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<boolean>;
/**
* 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);
};
}

View File

@ -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 || "",
});
}
}

View File

@ -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: [],

View File

@ -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"

View File

@ -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 = [];

View File

@ -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 () => {

View File

@ -48,7 +48,7 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService {
return {
mode,
cipherType,
cipherType: cipher?.type ?? cipherType ?? CipherType.Login,
admin: false,
allowPersonalOwnership,
originalCipher: cipher,