From 51763455847b6693af980d84e30981fbff444add Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 6 May 2025 10:50:48 -0500 Subject: [PATCH] [PM-19532] - Add Fingerprint phrase to cli auth request (#14556) * first pass at adding fingerprint phrase to auth requests * Moved call to getFingerprint into the service layer. Added a new method for getting auth requests. Updated tests. * Fixing the import * Renaming to WithFingerprint --- .../device-approval/list.command.ts | 5 +- .../pending-auth-request.response.ts | 6 +- .../organization-auth-request.service.spec.ts | 58 +++++++++++++++++++ .../organization-auth-request.service.ts | 11 ++++ ...ding-auth-request-with-fingerprint.view.ts | 27 +++++++++ .../device-approvals.component.html | 2 +- .../device-approvals.component.ts | 7 ++- 7 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 bitwarden_license/bit-common/src/admin-console/auth-requests/pending-auth-request-with-fingerprint.view.ts diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts index 31a0e748175..76b22d0edcb 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts @@ -47,7 +47,10 @@ export class ListCommand { try { const requests = - await this.organizationAuthRequestService.listPendingRequests(organizationId); + await this.organizationAuthRequestService.listPendingRequestsWithFingerprint( + organizationId, + ); + const res = new ListResponse(requests.map((r) => new PendingAuthRequestResponse(r))); return Response.success(res); } catch (e) { diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts index 991b3fb8e58..aa7b9c42773 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts @@ -1,4 +1,4 @@ -import { PendingAuthRequestView } from "@bitwarden/bit-common/admin-console/auth-requests/"; +import { PendingAuthRequestWithFingerprintView } from "@bitwarden/bit-common/admin-console/auth-requests/pending-auth-request-with-fingerprint.view"; import { BaseResponse } from "@bitwarden/cli/models/response/base.response"; export class PendingAuthRequestResponse implements BaseResponse { @@ -12,8 +12,9 @@ export class PendingAuthRequestResponse implements BaseResponse { requestDeviceType: string; requestIpAddress: string; creationDate: Date; + fingerprintPhrase: string; - constructor(authRequest: PendingAuthRequestView) { + constructor(authRequest: PendingAuthRequestWithFingerprintView) { this.id = authRequest.id; this.userId = authRequest.userId; this.organizationUserId = authRequest.organizationUserId; @@ -22,5 +23,6 @@ export class PendingAuthRequestResponse implements BaseResponse { this.requestDeviceType = authRequest.requestDeviceType; this.requestIpAddress = authRequest.requestIpAddress; this.creationDate = authRequest.creationDate; + this.fingerprintPhrase = authRequest.fingerprintPhrase; } } diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts index 933a5af1760..5d5decac67a 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts @@ -83,6 +83,64 @@ describe("OrganizationAuthRequestService", () => { }); }); + describe("listPendingRequestsWithDetails", () => { + it("should retrieve the fingerprint phrase for each request and return the new result", async () => { + jest.spyOn(organizationAuthRequestApiService, "listPendingRequests"); + + const organizationId = "organizationId"; + + const pendingAuthRequest = new PendingAuthRequestView(); + pendingAuthRequest.id = "requestId1"; + pendingAuthRequest.userId = "userId1"; + pendingAuthRequest.organizationUserId = "userId1"; + pendingAuthRequest.email = "email1"; + pendingAuthRequest.publicKey = "publicKey1"; + pendingAuthRequest.requestDeviceIdentifier = "requestDeviceIdentifier1"; + pendingAuthRequest.requestDeviceType = "requestDeviceType1"; + pendingAuthRequest.requestIpAddress = "requestIpAddress1"; + pendingAuthRequest.creationDate = new Date(); + const mockPendingAuthRequests = [pendingAuthRequest]; + organizationAuthRequestApiService.listPendingRequests + .calledWith(organizationId) + .mockResolvedValue(mockPendingAuthRequests); + + const fingerprintPhrase = ["fingerprint", "phrase"]; + keyService.getFingerprint + .calledWith(pendingAuthRequest.email, expect.any(Uint8Array)) + .mockResolvedValue(fingerprintPhrase); + + const result = + await organizationAuthRequestService.listPendingRequestsWithFingerprint(organizationId); + + expect(result).toHaveLength(1); + expect(result).toEqual([ + { ...pendingAuthRequest, fingerprintPhrase: fingerprintPhrase.join("-") }, + ]); + expect(organizationAuthRequestApiService.listPendingRequests).toHaveBeenCalledWith( + organizationId, + ); + }); + + it("should return empty list if no results and not call keyService", async () => { + jest.spyOn(organizationAuthRequestApiService, "listPendingRequests"); + + const organizationId = "organizationId"; + + organizationAuthRequestApiService.listPendingRequests + .calledWith(organizationId) + .mockResolvedValue([]); + + const result = + await organizationAuthRequestService.listPendingRequestsWithFingerprint(organizationId); + + expect(result).toHaveLength(0); + expect(keyService.getFingerprint).not.toHaveBeenCalled(); + expect(organizationAuthRequestApiService.listPendingRequests).toHaveBeenCalledWith( + organizationId, + ); + }); + }); + describe("denyPendingRequests", () => { it("should deny the specified pending auth requests", async () => { jest.spyOn(organizationAuthRequestApiService, "denyPendingRequests"); diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts index 97e271e770e..b7994c2b214 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts @@ -11,6 +11,7 @@ import { KeyService } from "@bitwarden/key-management"; import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service"; import { OrganizationAuthRequestUpdateRequest } from "./organization-auth-request-update.request"; +import { PendingAuthRequestWithFingerprintView } from "./pending-auth-request-with-fingerprint.view"; import { PendingAuthRequestView } from "./pending-auth-request.view"; export class OrganizationAuthRequestService { @@ -25,6 +26,16 @@ export class OrganizationAuthRequestService { return await this.organizationAuthRequestApiService.listPendingRequests(organizationId); } + async listPendingRequestsWithFingerprint( + organizationId: string, + ): Promise { + return Promise.all( + ((await this.listPendingRequests(organizationId)) ?? []).map( + async (r) => await PendingAuthRequestWithFingerprintView.fromView(r, this.keyService), + ), + ); + } + async denyPendingRequests(organizationId: string, ...requestIds: string[]): Promise { await this.organizationAuthRequestApiService.denyPendingRequests(organizationId, ...requestIds); } diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/pending-auth-request-with-fingerprint.view.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/pending-auth-request-with-fingerprint.view.ts new file mode 100644 index 00000000000..af89bf33ba7 --- /dev/null +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/pending-auth-request-with-fingerprint.view.ts @@ -0,0 +1,27 @@ +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { KeyService } from "@bitwarden/key-management"; + +import { PendingAuthRequestView } from "./pending-auth-request.view"; + +export class PendingAuthRequestWithFingerprintView extends PendingAuthRequestView { + fingerprintPhrase: string = ""; + + static async fromView( + view: PendingAuthRequestView, + keyService: KeyService, + ): Promise { + const requestWithDetailsView = Object.assign( + new PendingAuthRequestWithFingerprintView(), + view, + ) as PendingAuthRequestWithFingerprintView; + + requestWithDetailsView.fingerprintPhrase = ( + await keyService.getFingerprint( + requestWithDetailsView.email, + Utils.fromB64ToArray(requestWithDetailsView.publicKey), + ) + )?.join("-"); + + return requestWithDetailsView; + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html index cafd0744a8f..48463b7359e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html @@ -56,7 +56,7 @@
{{ r.email }}
- {{ r.publicKey | fingerprint: r.email | async }} + {{ r.fingerprintPhrase }}
{{ r.requestDeviceType }}
diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts index 744cf2c4674..83f23089c59 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts @@ -8,6 +8,7 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { OrganizationAuthRequestApiService } from "@bitwarden/bit-common/admin-console/auth-requests/organization-auth-request-api.service"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests/organization-auth-request.service"; +import { PendingAuthRequestWithFingerprintView } from "@bitwarden/bit-common/admin-console/auth-requests/pending-auth-request-with-fingerprint.view"; import { PendingAuthRequestView } from "@bitwarden/bit-common/admin-console/auth-requests/pending-auth-request.view"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; @@ -44,7 +45,7 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; imports: [SharedModule, NoItemsModule, LooseComponentsModule], }) export class DeviceApprovalsComponent implements OnInit, OnDestroy { - tableDataSource = new TableDataSource(); + tableDataSource = new TableDataSource(); organizationId: string; loading = true; actionInProgress = false; @@ -73,7 +74,9 @@ export class DeviceApprovalsComponent implements OnInit, OnDestroy { this.refresh$.pipe( tap(() => (this.loading = true)), switchMap(() => - this.organizationAuthRequestService.listPendingRequests(this.organizationId), + this.organizationAuthRequestService.listPendingRequestsWithFingerprint( + this.organizationId, + ), ), ), ),