1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

[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
This commit is contained in:
Jared McCannon
2025-05-06 10:50:48 -05:00
committed by GitHub
parent 855dad7fcc
commit 5176345584
7 changed files with 110 additions and 6 deletions

View File

@@ -47,7 +47,10 @@ export class ListCommand {
try { try {
const requests = const requests =
await this.organizationAuthRequestService.listPendingRequests(organizationId); await this.organizationAuthRequestService.listPendingRequestsWithFingerprint(
organizationId,
);
const res = new ListResponse(requests.map((r) => new PendingAuthRequestResponse(r))); const res = new ListResponse(requests.map((r) => new PendingAuthRequestResponse(r)));
return Response.success(res); return Response.success(res);
} catch (e) { } catch (e) {

View File

@@ -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"; import { BaseResponse } from "@bitwarden/cli/models/response/base.response";
export class PendingAuthRequestResponse implements BaseResponse { export class PendingAuthRequestResponse implements BaseResponse {
@@ -12,8 +12,9 @@ export class PendingAuthRequestResponse implements BaseResponse {
requestDeviceType: string; requestDeviceType: string;
requestIpAddress: string; requestIpAddress: string;
creationDate: Date; creationDate: Date;
fingerprintPhrase: string;
constructor(authRequest: PendingAuthRequestView) { constructor(authRequest: PendingAuthRequestWithFingerprintView) {
this.id = authRequest.id; this.id = authRequest.id;
this.userId = authRequest.userId; this.userId = authRequest.userId;
this.organizationUserId = authRequest.organizationUserId; this.organizationUserId = authRequest.organizationUserId;
@@ -22,5 +23,6 @@ export class PendingAuthRequestResponse implements BaseResponse {
this.requestDeviceType = authRequest.requestDeviceType; this.requestDeviceType = authRequest.requestDeviceType;
this.requestIpAddress = authRequest.requestIpAddress; this.requestIpAddress = authRequest.requestIpAddress;
this.creationDate = authRequest.creationDate; this.creationDate = authRequest.creationDate;
this.fingerprintPhrase = authRequest.fingerprintPhrase;
} }
} }

View File

@@ -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", () => { describe("denyPendingRequests", () => {
it("should deny the specified pending auth requests", async () => { it("should deny the specified pending auth requests", async () => {
jest.spyOn(organizationAuthRequestApiService, "denyPendingRequests"); jest.spyOn(organizationAuthRequestApiService, "denyPendingRequests");

View File

@@ -11,6 +11,7 @@ import { KeyService } from "@bitwarden/key-management";
import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service"; import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service";
import { OrganizationAuthRequestUpdateRequest } from "./organization-auth-request-update.request"; import { OrganizationAuthRequestUpdateRequest } from "./organization-auth-request-update.request";
import { PendingAuthRequestWithFingerprintView } from "./pending-auth-request-with-fingerprint.view";
import { PendingAuthRequestView } from "./pending-auth-request.view"; import { PendingAuthRequestView } from "./pending-auth-request.view";
export class OrganizationAuthRequestService { export class OrganizationAuthRequestService {
@@ -25,6 +26,16 @@ export class OrganizationAuthRequestService {
return await this.organizationAuthRequestApiService.listPendingRequests(organizationId); return await this.organizationAuthRequestApiService.listPendingRequests(organizationId);
} }
async listPendingRequestsWithFingerprint(
organizationId: string,
): Promise<PendingAuthRequestWithFingerprintView[]> {
return Promise.all(
((await this.listPendingRequests(organizationId)) ?? []).map(
async (r) => await PendingAuthRequestWithFingerprintView.fromView(r, this.keyService),
),
);
}
async denyPendingRequests(organizationId: string, ...requestIds: string[]): Promise<void> { async denyPendingRequests(organizationId: string, ...requestIds: string[]): Promise<void> {
await this.organizationAuthRequestApiService.denyPendingRequests(organizationId, ...requestIds); await this.organizationAuthRequestApiService.denyPendingRequests(organizationId, ...requestIds);
} }

View File

@@ -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<PendingAuthRequestWithFingerprintView> {
const requestWithDetailsView = Object.assign(
new PendingAuthRequestWithFingerprintView(),
view,
) as PendingAuthRequestWithFingerprintView;
requestWithDetailsView.fingerprintPhrase = (
await keyService.getFingerprint(
requestWithDetailsView.email,
Utils.fromB64ToArray(requestWithDetailsView.publicKey),
)
)?.join("-");
return requestWithDetailsView;
}
}

View File

@@ -56,7 +56,7 @@
<tr bitRow alignContent="top" *ngFor="let r of rows$ | async"> <tr bitRow alignContent="top" *ngFor="let r of rows$ | async">
<td bitCell class="tw-flex-col"> <td bitCell class="tw-flex-col">
<div>{{ r.email }}</div> <div>{{ r.email }}</div>
<code class="tw-text-sm">{{ r.publicKey | fingerprint: r.email | async }}</code> <code class="tw-text-sm">{{ r.fingerprintPhrase }}</code>
</td> </td>
<td bitCell class="tw-flex-col"> <td bitCell class="tw-flex-col">
<div>{{ r.requestDeviceType }}</div> <div>{{ r.requestDeviceType }}</div>

View File

@@ -8,6 +8,7 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; 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 { 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 { 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 { PendingAuthRequestView } from "@bitwarden/bit-common/admin-console/auth-requests/pending-auth-request.view";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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], imports: [SharedModule, NoItemsModule, LooseComponentsModule],
}) })
export class DeviceApprovalsComponent implements OnInit, OnDestroy { export class DeviceApprovalsComponent implements OnInit, OnDestroy {
tableDataSource = new TableDataSource<PendingAuthRequestView>(); tableDataSource = new TableDataSource<PendingAuthRequestWithFingerprintView>();
organizationId: string; organizationId: string;
loading = true; loading = true;
actionInProgress = false; actionInProgress = false;
@@ -73,7 +74,9 @@ export class DeviceApprovalsComponent implements OnInit, OnDestroy {
this.refresh$.pipe( this.refresh$.pipe(
tap(() => (this.loading = true)), tap(() => (this.loading = true)),
switchMap(() => switchMap(() =>
this.organizationAuthRequestService.listPendingRequests(this.organizationId), this.organizationAuthRequestService.listPendingRequestsWithFingerprint(
this.organizationId,
),
), ),
), ),
), ),