mirror of
https://github.com/bitwarden/browser
synced 2025-01-20 16:31:15 +01: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:
parent
00f6920a86
commit
ad01a529e8
@ -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,
|
||||
|
@ -10,7 +10,8 @@
|
||||
*ngIf="!loading"
|
||||
formId="cipherForm"
|
||||
[config]="config"
|
||||
(cipherSaved)="onCipherSaved()"
|
||||
(cipherSaved)="onCipherSaved($event)"
|
||||
[beforeSubmit]="checkFido2UserVerification"
|
||||
[submitBtn]="submitBtn"
|
||||
>
|
||||
<app-open-attachments
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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) =>
|
||||
<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",
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ export type OptionalInitialValues = {
|
||||
organizationId?: OrganizationId;
|
||||
collectionIds?: CollectionId[];
|
||||
loginUri?: string;
|
||||
username?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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 || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: [],
|
||||
|
@ -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"
|
||||
|
@ -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 = [];
|
||||
|
||||
|
@ -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 () => {
|
||||
|
@ -48,7 +48,7 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService {
|
||||
|
||||
return {
|
||||
mode,
|
||||
cipherType,
|
||||
cipherType: cipher?.type ?? cipherType ?? CipherType.Login,
|
||||
admin: false,
|
||||
allowPersonalOwnership,
|
||||
originalCipher: cipher,
|
||||
|
Loading…
Reference in New Issue
Block a user