mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 15:23:33 +00:00
[PM-10329] Add auth request api call to desktop component v2 (#15535)
* fix: - add auth request api call to desktop component v2 - move logic to auth request service * test: added tests for new auth request service method
This commit is contained in:
@@ -15,6 +15,7 @@ import { filter, map, take } from "rxjs/operators";
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -23,7 +24,9 @@ import { Account, AccountService } from "@bitwarden/common/auth/abstractions/acc
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -194,6 +197,8 @@ export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener {
|
||||
private collectionService: CollectionService,
|
||||
private organizationService: OrganizationService,
|
||||
private folderService: FolderService,
|
||||
private configService: ConfigService,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -303,11 +308,26 @@ export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener {
|
||||
this.searchBarService.setEnabled(true);
|
||||
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
|
||||
|
||||
const authRequest = await this.apiService.getLastAuthRequest().catch(() => null);
|
||||
if (authRequest != null) {
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: authRequest.id,
|
||||
});
|
||||
if (
|
||||
(await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval),
|
||||
)) === true
|
||||
) {
|
||||
const authRequests = await firstValueFrom(
|
||||
this.authRequestService.getLatestPendingAuthRequest$(),
|
||||
);
|
||||
if (authRequests != null) {
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: authRequests.id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const authRequest = await this.apiService.getLastAuthRequest();
|
||||
if (authRequest != null) {
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: authRequest.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.activeUserId = await firstValueFrom(
|
||||
|
||||
@@ -47,6 +47,12 @@ export abstract class AuthRequestServiceAbstraction {
|
||||
* The array will be empty if there are no pending auth requests.
|
||||
*/
|
||||
abstract getPendingAuthRequests$(): Observable<Array<AuthRequestResponse>>;
|
||||
/**
|
||||
* Get the most recent AuthRequest for the logged in user
|
||||
* @returns An observable of an auth request. If there are no auth requests
|
||||
* the result will be null.
|
||||
*/
|
||||
abstract getLatestPendingAuthRequest$(): Observable<AuthRequestResponse> | null;
|
||||
/**
|
||||
* Approve or deny an auth request.
|
||||
* @param approve True to approve, false to deny.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -120,6 +122,7 @@ describe("AuthRequestService", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setUserKeyAfterDecryptingSharedUserKey", () => {
|
||||
it("decrypts and sets user key when given valid auth request response and private key", async () => {
|
||||
// Arrange
|
||||
@@ -237,4 +240,99 @@ describe("AuthRequestService", () => {
|
||||
expect(phrase).toEqual(phraseUpperCase);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLatestAuthRequest", () => {
|
||||
it("returns newest authRequest from list of authRequests", async () => {
|
||||
const now = minutesAgo(0);
|
||||
const fiveMinutesAgo = minutesAgo(5);
|
||||
const tenMinutesAgo = minutesAgo(10);
|
||||
|
||||
const newerAuthRequest = createMockAuthRequest(
|
||||
"now-request",
|
||||
false,
|
||||
false,
|
||||
now.toISOString(), // newer request
|
||||
"1fda13f4-5134-4157-90e3-b4e3fb2d855z",
|
||||
);
|
||||
const olderAuthRequest = createMockAuthRequest(
|
||||
"5-minute-old-request",
|
||||
false,
|
||||
false,
|
||||
fiveMinutesAgo.toISOString(), // older request
|
||||
"1fda13f4-5134-4157-90e3-b4e3fb2d855c",
|
||||
);
|
||||
const oldestAuthRequest = createMockAuthRequest(
|
||||
"10-minute-old-request",
|
||||
false,
|
||||
false,
|
||||
tenMinutesAgo.toISOString(), // oldest request
|
||||
"1fda13f4-5134-4157-90e3-b4e3fb2d855a",
|
||||
);
|
||||
|
||||
const listResponse = new ListResponse(
|
||||
{ Data: [oldestAuthRequest, olderAuthRequest, newerAuthRequest] },
|
||||
AuthRequestResponse,
|
||||
);
|
||||
|
||||
// Ensure the mock is properly set up to return the list response
|
||||
authRequestApiService.getPendingAuthRequests.mockResolvedValue(listResponse);
|
||||
|
||||
// Act
|
||||
const sutReturnValue = await firstValueFrom(sut.getLatestPendingAuthRequest$());
|
||||
|
||||
// Assert
|
||||
// Verify the mock was called
|
||||
expect(authRequestApiService.getPendingAuthRequests).toHaveBeenCalledTimes(1);
|
||||
expect(sutReturnValue.creationDate).toEqual(newerAuthRequest.creationDate);
|
||||
expect(sutReturnValue.id).toEqual(newerAuthRequest.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null from empty list of authRequests", async () => {
|
||||
const listResponse = new ListResponse({ Data: [] }, AuthRequestResponse);
|
||||
|
||||
// Ensure the mock is properly set up to return the list response
|
||||
authRequestApiService.getPendingAuthRequests.mockResolvedValue(listResponse);
|
||||
|
||||
// Act
|
||||
const sutReturnValue = await firstValueFrom(sut.getLatestPendingAuthRequest$());
|
||||
|
||||
// Assert
|
||||
// Verify the mock was called
|
||||
expect(authRequestApiService.getPendingAuthRequests).toHaveBeenCalledTimes(1);
|
||||
expect(sutReturnValue).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function createMockAuthRequest(
|
||||
id: string,
|
||||
isAnswered: boolean,
|
||||
isExpired: boolean,
|
||||
creationDate: string,
|
||||
deviceId?: string,
|
||||
): AuthRequestResponse {
|
||||
const authRequestResponse = new AuthRequestResponse({
|
||||
id: id,
|
||||
publicKey:
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+AIKUBDf4exqE9JDzGJegDzIoaZcNkUeewovgwSJuKuya0mP4CPP00ajmi9GEu6z3VWfB+yzx1O4gxHV/T5s620wnMYm6nAv2gDS+kEaXou4MOt7QMidq4kVhM7aixN2klKivH/E8GFPiMUzNQv0lMQthsVLLWFuMRxYfChe9Cxn9EWp7TYy4rAmi+jSTxzIGj+RC7f2qu2qdPSsKHLXtW7NA0SWhIntWbmc9QxD2nQ4qHgk/qUwvHoUhwKGNCcIDkXqMJ7ChN3v5tX1sFpwhQQrmlwiVC4+sBScfAgyYylfTPnuBd6b3UrC3D34GvHMgDvLjz7LwlBrkSXoF7xWZwIDAQAB",
|
||||
requestDeviceIdentifier: "1fda13f4-5134-4157-90e3-b4e3fb2d855c",
|
||||
requestDeviceTypeValue: 10,
|
||||
requestDeviceType: "Firefox",
|
||||
requestIpAddress: "2a04:4e40:9400:0:bb4:3591:d601:f5cc",
|
||||
requestCountryName: "united states",
|
||||
key: null,
|
||||
masterPasswordHash: null,
|
||||
creationDate: creationDate, // ISO 8601 date string : "2025-07-11T19:11:17.9866667Z"
|
||||
responseDate: null,
|
||||
requestApproved: false,
|
||||
isAnswered: isAnswered,
|
||||
isExpired: isExpired,
|
||||
deviceId: deviceId,
|
||||
});
|
||||
|
||||
return authRequestResponse;
|
||||
}
|
||||
|
||||
function minutesAgo(minutes: number): Date {
|
||||
return new Date(Date.now() - minutes * 60_000);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,21 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
getLatestPendingAuthRequest$(): Observable<AuthRequestResponse | null> {
|
||||
return this.getPendingAuthRequests$().pipe(
|
||||
map((authRequests: Array<AuthRequestResponse>) => {
|
||||
if (authRequests.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return authRequests.sort((a, b) => {
|
||||
const dateA = new Date(a.creationDate).getTime();
|
||||
const dateB = new Date(b.creationDate).getTime();
|
||||
return dateB - dateA; // Sort in descending order
|
||||
})[0];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async approveOrDenyAuthRequest(
|
||||
approve: boolean,
|
||||
authRequest: AuthRequestResponse,
|
||||
|
||||
Reference in New Issue
Block a user