diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f30ea57319..2e4818a8b0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -6,8 +6,10 @@ import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs"; import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; import { + AuthRequestApiServiceAbstraction, AuthRequestService, AuthRequestServiceAbstraction, + DefaultAuthRequestApiService, DefaultLockService, InternalUserDecryptionOptionsServiceAbstraction, LoginEmailServiceAbstraction, @@ -375,6 +377,7 @@ export default class MainBackground { devicesService: DevicesServiceAbstraction; deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestServiceAbstraction; + authRequestApiService: AuthRequestApiServiceAbstraction; accountService: AccountServiceAbstraction; globalStateProvider: GlobalStateProvider; pinService: PinServiceAbstraction; @@ -813,14 +816,16 @@ export default class MainBackground { this.appIdService, ); + this.authRequestApiService = new DefaultAuthRequestApiService(this.apiService, this.logService); + this.authRequestService = new AuthRequestService( this.appIdService, - this.accountService, this.masterPasswordService, this.keyService, this.encryptService, this.apiService, this.stateProvider, + this.authRequestApiService, ); this.authService = new AuthService( diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index d2cc729e48..099ce503fa 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -20,6 +20,8 @@ import { PinServiceAbstraction, UserDecryptionOptionsService, SsoUrlService, + AuthRequestApiServiceAbstraction, + DefaultAuthRequestApiService, } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -265,6 +267,7 @@ export class ServiceContainer { devicesApiService: DevicesApiServiceAbstraction; deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestService; + authRequestApiService: AuthRequestApiServiceAbstraction; configApiService: ConfigApiServiceAbstraction; configService: ConfigService; accountService: AccountService; @@ -616,14 +619,16 @@ export class ServiceContainer { this.stateProvider, ); + this.authRequestApiService = new DefaultAuthRequestApiService(this.apiService, this.logService); + this.authRequestService = new AuthRequestService( this.appIdService, - this.accountService, this.masterPasswordService, this.keyService, this.encryptService, this.apiService, this.stateProvider, + this.authRequestApiService, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index 0d66dbc7d7..d8a54f1ec3 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -16,13 +16,16 @@ import { filter, first, map, take } from "rxjs/operators"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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"; @@ -120,6 +123,8 @@ export class VaultComponent implements OnInit, OnDestroy { private accountService: AccountService, private cipherService: CipherService, private folderService: FolderService, + private authRequestService: AuthRequestServiceAbstraction, + private configService: ConfigService, ) {} async ngOnInit() { @@ -237,11 +242,30 @@ export class VaultComponent implements OnInit, OnDestroy { this.searchBarService.setEnabled(true); this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); - const authRequest = await this.apiService.getLastAuthRequest(); - if (authRequest != null) { - this.messagingService.send("openLoginApproval", { - notificationId: authRequest.id, - }); + const browserLoginApprovalFeatureFlag = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval), + ); + if (browserLoginApprovalFeatureFlag === true) { + const authRequests = await firstValueFrom(this.authRequestService.getPendingAuthRequests$()); + // There is a chance that there is more than one auth request in the response we only show the most recent one + if (authRequests.length > 0) { + const mostRecentAuthRequest = authRequests.reduce((latest, current) => { + const latestDate = new Date(latest.creationDate).getTime(); + const currentDate = new Date(current.creationDate).getTime(); + return currentDate > latestDate ? current : latest; + }); + + this.messagingService.send("openLoginApproval", { + notificationId: mostRecentAuthRequest.id, + }); + } + } else { + const authRequest = await this.apiService.getLastAuthRequest(); + if (authRequest != null) { + this.messagingService.send("openLoginApproval", { + notificationId: authRequest.id, + }); + } } this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); diff --git a/apps/web/src/app/auth/settings/security/device-management.component.spec.ts b/apps/web/src/app/auth/settings/security/device-management.component.spec.ts index d86123f52b..2821d4a6d7 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.spec.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { RouterTestingModule } from "@angular/router/testing"; import { of, Subject } from "rxjs"; -import { AuthRequestApiService } from "@bitwarden/auth/common"; +import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; import { DeviceType } from "@bitwarden/common/enums"; @@ -79,7 +79,7 @@ describe("DeviceManagementComponent", () => { }, }, { - provide: AuthRequestApiService, + provide: AuthRequestApiServiceAbstraction, useValue: { getAuthRequest: jest.fn().mockResolvedValue(mockDeviceResponse), }, diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index c831d26ea1..854a13faa9 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -4,7 +4,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { firstValueFrom } from "rxjs"; import { LoginApprovalComponent } from "@bitwarden/auth/angular"; -import { AuthRequestApiService } from "@bitwarden/auth/common"; +import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicePendingAuthRequest, @@ -61,7 +61,7 @@ export class DeviceManagementComponent { private toastService: ToastService, private validationService: ValidationService, private messageListener: MessageListener, - private authRequestApiService: AuthRequestApiService, + private authRequestApiService: AuthRequestApiServiceAbstraction, private destroyRef: DestroyRef, ) { void this.initializeDevices(); diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts index 4ce65b9f77..c97b23b145 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts @@ -1,16 +1,19 @@ import { TestBed } from "@angular/core/testing"; -import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs"; import { + AuthRequestServiceAbstraction, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; -import { DeviceResponse } from "@bitwarden/common/auth/abstractions/devices/responses/device.response"; import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; +import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DeviceType } from "@bitwarden/common/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -41,11 +44,15 @@ describe("VaultBannersService", () => { [userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo, }); const devices$ = new BehaviorSubject([]); + const pendingAuthRequests$ = new BehaviorSubject>([]); + let configService: MockProxy; beforeEach(() => { lastSync$.next(new Date("2024-05-14")); isSelfHost.mockClear(); getEmailVerified.mockClear().mockResolvedValue(true); + configService = mock(); + configService.getFeatureFlag$.mockImplementation(() => of(true)); TestBed.configureTestingModule({ providers: [ @@ -88,6 +95,14 @@ describe("VaultBannersService", () => { provide: DevicesServiceAbstraction, useValue: { getDevices$: () => devices$ }, }, + { + provide: AuthRequestServiceAbstraction, + useValue: { getPendingAuthRequests$: () => pendingAuthRequests$ }, + }, + { + provide: ConfigService, + useValue: configService, + }, ], }); }); @@ -286,31 +301,25 @@ describe("VaultBannersService", () => { describe("PendingAuthRequest", () => { const now = new Date(); - let deviceResponse: DeviceResponse; + let authRequestResponse: AuthRequestResponse; beforeEach(() => { - deviceResponse = new DeviceResponse({ - Id: "device1", - UserId: userId, - Name: "Test Device", - Identifier: "test-device", - Type: DeviceType.Android, - CreationDate: now.toISOString(), - RevisionDate: now.toISOString(), - IsTrusted: false, + authRequestResponse = new AuthRequestResponse({ + id: "authRequest1", + deviceId: "device1", + deviceName: "Test Device", + deviceType: DeviceType.Android, + creationDate: now.toISOString(), + requestApproved: null, }); // Reset devices list, single user state, and active user state before each test - devices$.next([]); + pendingAuthRequests$.next([]); fakeStateProvider.singleUser.states.clear(); fakeStateProvider.activeUser.states.clear(); }); it("shows pending auth request banner when there is a pending request", async () => { - deviceResponse.devicePendingAuthRequest = { - id: "123", - creationDate: now.toISOString(), - }; - devices$.next([new DeviceView(deviceResponse)]); + pendingAuthRequests$.next([new AuthRequestResponse(authRequestResponse)]); service = TestBed.inject(VaultBannersService); @@ -318,8 +327,7 @@ describe("VaultBannersService", () => { }); it("does not show pending auth request banner when there are no pending requests", async () => { - deviceResponse.devicePendingAuthRequest = null; - devices$.next([new DeviceView(deviceResponse)]); + pendingAuthRequests$.next([]); service = TestBed.inject(VaultBannersService); @@ -327,11 +335,7 @@ describe("VaultBannersService", () => { }); it("dismisses pending auth request banner", async () => { - deviceResponse.devicePendingAuthRequest = { - id: "123", - creationDate: now.toISOString(), - }; - devices$.next([new DeviceView(deviceResponse)]); + pendingAuthRequests$.next([new AuthRequestResponse(authRequestResponse)]); service = TestBed.inject(VaultBannersService); diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index 17aaf5271b..dd50c832cc 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -1,10 +1,15 @@ import { Injectable } from "@angular/core"; import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs"; -import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { + AuthRequestServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateProvider, @@ -66,20 +71,33 @@ export class VaultBannersService { private syncService: SyncService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private devicesService: DevicesServiceAbstraction, + private authRequestService: AuthRequestServiceAbstraction, + private configService: ConfigService, ) {} /** Returns true when the pending auth request banner should be shown */ async shouldShowPendingAuthRequestBanner(userId: UserId): Promise { - const devices = await firstValueFrom(this.devicesService.getDevices$()); - const hasPendingRequest = devices.some( - (device) => device.response?.devicePendingAuthRequest != null, - ); - const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes( VisibleVaultBanner.PendingAuthRequest, ); + // TODO: PM-20439 remove feature flag + const browserLoginApprovalFeatureFlag = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval), + ); + if (browserLoginApprovalFeatureFlag === true) { + const pendingAuthRequests = await firstValueFrom( + this.authRequestService.getPendingAuthRequests$(), + ); - return hasPendingRequest && !alreadyDismissed; + return pendingAuthRequests.length > 0 && !alreadyDismissed; + } else { + const devices = await firstValueFrom(this.devicesService.getDevices$()); + const hasPendingRequest = devices.some( + (device) => device.response?.devicePendingAuthRequest != null, + ); + + return hasPendingRequest && !alreadyDismissed; + } } shouldShowPremiumBanner$(userId: UserId): Observable { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 559761bd1b..bec32ac115 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -35,7 +35,7 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { - AuthRequestApiService, + AuthRequestApiServiceAbstraction, AuthRequestService, AuthRequestServiceAbstraction, DefaultAuthRequestApiService, @@ -1181,6 +1181,11 @@ const safeProviders: SafeProvider[] = [ useClass: DevicesServiceImplementation, deps: [DevicesApiServiceAbstraction, AppIdServiceAbstraction], }), + safeProvider({ + provide: AuthRequestApiServiceAbstraction, + useClass: DefaultAuthRequestApiService, + deps: [ApiServiceAbstraction, LogService], + }), safeProvider({ provide: DeviceTrustServiceAbstraction, useClass: DeviceTrustService, @@ -1205,12 +1210,12 @@ const safeProviders: SafeProvider[] = [ useClass: AuthRequestService, deps: [ AppIdServiceAbstraction, - AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, KeyService, EncryptService, ApiServiceAbstraction, StateProvider, + AuthRequestApiServiceAbstraction, ], }), safeProvider({ @@ -1477,11 +1482,6 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultCipherAuthorizationService, deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction], }), - safeProvider({ - provide: AuthRequestApiService, - useClass: DefaultAuthRequestApiService, - deps: [ApiServiceAbstraction, LogService], - }), safeProvider({ provide: LoginApprovalComponentServiceAbstraction, useClass: DefaultLoginApprovalComponentService, diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index 9912c45e9d..5e410c538f 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -39,7 +39,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { AuthRequestApiService } from "../../common/abstractions/auth-request-api.service"; +import { AuthRequestApiServiceAbstraction } from "../../common/abstractions/auth-request-api.service"; import { LoginViaAuthRequestCacheService } from "../../common/services/auth-request/default-login-via-auth-request-cache.service"; // FIXME: update to use a const object instead of a typescript enum @@ -85,7 +85,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private accountService: AccountService, private anonymousHubService: AnonymousHubService, private appIdService: AppIdService, - private authRequestApiService: AuthRequestApiService, + private authRequestApiService: AuthRequestApiServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private authService: AuthService, private cryptoFunctionService: CryptoFunctionService, diff --git a/libs/auth/src/common/abstractions/auth-request-api.service.ts b/libs/auth/src/common/abstractions/auth-request-api.service.ts index 1b0befc0df..6a6358fa2c 100644 --- a/libs/auth/src/common/abstractions/auth-request-api.service.ts +++ b/libs/auth/src/common/abstractions/auth-request-api.service.ts @@ -1,7 +1,16 @@ import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; + +export abstract class AuthRequestApiServiceAbstraction { + /** + * Gets a list of pending auth requests based on the user. There will only be one AuthRequest per device and the + * AuthRequest will be the most recent pending request. + * + * @returns A promise that resolves to a list response containing auth request responses. + */ + abstract getPendingAuthRequests(): Promise>; -export abstract class AuthRequestApiService { /** * Gets an auth request by its ID. * diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 75bb868616..956fd77103 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -41,6 +41,12 @@ export abstract class AuthRequestServiceAbstraction { * @throws If `userId` is not provided. */ abstract clearAdminAuthRequest: (userId: UserId) => Promise; + /** + * Gets a list of standard pending auth requests for the user. + * @returns An observable of an array of auth request. + * The array will be empty if there are no pending auth requests. + */ + abstract getPendingAuthRequests$(): Observable>; /** * Approve or deny an auth request. * @param approve True to approve, false to deny. diff --git a/libs/auth/src/common/services/auth-request/auth-request-api.service.ts b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts index c9fec1400c..15517a9a0e 100644 --- a/libs/auth/src/common/services/auth-request/auth-request-api.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts @@ -1,16 +1,23 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { AuthRequestApiService } from "../../abstractions/auth-request-api.service"; +import { AuthRequestApiServiceAbstraction } from "../../abstractions/auth-request-api.service"; -export class DefaultAuthRequestApiService implements AuthRequestApiService { +export class DefaultAuthRequestApiService implements AuthRequestApiServiceAbstraction { constructor( private apiService: ApiService, private logService: LogService, ) {} + async getPendingAuthRequests(): Promise> { + const path = `/auth-requests/pending`; + const r = await this.apiService.send("GET", path, null, true, true); + return new ListResponse(r, AuthRequestResponse); + } + async getAuthRequest(requestId: string): Promise { try { const path = `/auth-requests/${requestId}`; diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index c3d6f78f3c..ab09e17f11 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -10,23 +10,23 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "@bitwarden/common/platform/state"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { DefaultAuthRequestApiService } from "./auth-request-api.service"; import { AuthRequestService } from "./auth-request.service"; describe("AuthRequestService", () => { let sut: AuthRequestService; const stateProvider = mock(); - let accountService: FakeAccountService; let masterPasswordService: FakeMasterPasswordService; const appIdService = mock(); const keyService = mock(); const encryptService = mock(); const apiService = mock(); + const authRequestApiService = mock(); let mockPrivateKey: Uint8Array; let mockPublicKey: Uint8Array; @@ -34,17 +34,16 @@ describe("AuthRequestService", () => { beforeEach(() => { jest.clearAllMocks(); - accountService = mockAccountServiceWith(mockUserId); masterPasswordService = new FakeMasterPasswordService(); sut = new AuthRequestService( appIdService, - accountService, masterPasswordService, keyService, encryptService, apiService, stateProvider, + authRequestApiService, ); mockPrivateKey = new Uint8Array(64); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index fca68b76bb..93a6ba12ff 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -1,15 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Observable, Subject, firstValueFrom } from "rxjs"; +import { Observable, Subject, defer, firstValueFrom, map } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +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"; @@ -24,6 +24,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { AuthRequestApiServiceAbstraction } from "../../abstractions/auth-request-api.service"; import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction"; /** @@ -49,12 +50,12 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { constructor( private appIdService: AppIdService, - private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, private keyService: KeyService, private encryptService: EncryptService, private apiService: ApiService, private stateProvider: StateProvider, + private authRequestApiService: AuthRequestApiServiceAbstraction, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); this.adminLoginApproved$ = this.adminLoginApprovedSubject.asObservable(); @@ -91,6 +92,19 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, null, userId); } + /** + * @description Gets the list of all standard (not admin approval) pending AuthRequests. + */ + getPendingAuthRequests$(): Observable> { + return defer(() => this.authRequestApiService.getPendingAuthRequests()).pipe( + map((authRequestResponses: ListResponse) => { + return authRequestResponses.data.map((authRequestResponse: AuthRequestResponse) => { + return new AuthRequestResponse(authRequestResponse); + }); + }), + ); + } + async approveOrDenyAuthRequest( approve: boolean, authRequest: AuthRequestResponse, diff --git a/libs/common/src/auth/models/response/auth-request.response.ts b/libs/common/src/auth/models/response/auth-request.response.ts index 372ae047f4..94c6500091 100644 --- a/libs/common/src/auth/models/response/auth-request.response.ts +++ b/libs/common/src/auth/models/response/auth-request.response.ts @@ -18,6 +18,7 @@ export class AuthRequestResponse extends BaseResponse { responseDate?: string; isAnswered: boolean; isExpired: boolean; + deviceId?: string; // could be null or empty constructor(response: any) { super(response); @@ -33,6 +34,7 @@ export class AuthRequestResponse extends BaseResponse { this.creationDate = this.getResponseProperty("CreationDate"); this.requestApproved = this.getResponseProperty("RequestApproved"); this.responseDate = this.getResponseProperty("ResponseDate"); + this.deviceId = this.getResponseProperty("RequestDeviceId"); const requestDate = new Date(this.creationDate); const requestDateUTC = Date.UTC( diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index d48c2185f2..8322dba03c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -19,6 +19,7 @@ export enum FeatureFlag { PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor", PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor", PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence", + PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals", /* Autofill */ BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", @@ -105,6 +106,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE, [FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE, [FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE, + [FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE, /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE,