1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 05:43:41 +00:00

[PM -20329] browser auth approval client api service (#15161)

* feat: Create methods for calling GET auth-request/pending endpoint.

* feat: update banner service on web, and desktop vault

* test: updated banner test to use auth request services

* fix: DI fixes

* feat: add RequestDeviceId to AuthRequestResponse

* fix: add Browser Approvals feature flags to desktop vault and web vault banner service

* test: fix tests for feature flag
This commit is contained in:
Ike
2025-06-26 11:13:06 -04:00
committed by GitHub
parent 4d0ad3310e
commit 7c9e95271d
16 changed files with 157 additions and 62 deletions

View File

@@ -6,8 +6,10 @@ import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs";
import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common";
import { import {
AuthRequestApiServiceAbstraction,
AuthRequestService, AuthRequestService,
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
DefaultAuthRequestApiService,
DefaultLockService, DefaultLockService,
InternalUserDecryptionOptionsServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction,
LoginEmailServiceAbstraction, LoginEmailServiceAbstraction,
@@ -375,6 +377,7 @@ export default class MainBackground {
devicesService: DevicesServiceAbstraction; devicesService: DevicesServiceAbstraction;
deviceTrustService: DeviceTrustServiceAbstraction; deviceTrustService: DeviceTrustServiceAbstraction;
authRequestService: AuthRequestServiceAbstraction; authRequestService: AuthRequestServiceAbstraction;
authRequestApiService: AuthRequestApiServiceAbstraction;
accountService: AccountServiceAbstraction; accountService: AccountServiceAbstraction;
globalStateProvider: GlobalStateProvider; globalStateProvider: GlobalStateProvider;
pinService: PinServiceAbstraction; pinService: PinServiceAbstraction;
@@ -813,14 +816,16 @@ export default class MainBackground {
this.appIdService, this.appIdService,
); );
this.authRequestApiService = new DefaultAuthRequestApiService(this.apiService, this.logService);
this.authRequestService = new AuthRequestService( this.authRequestService = new AuthRequestService(
this.appIdService, this.appIdService,
this.accountService,
this.masterPasswordService, this.masterPasswordService,
this.keyService, this.keyService,
this.encryptService, this.encryptService,
this.apiService, this.apiService,
this.stateProvider, this.stateProvider,
this.authRequestApiService,
); );
this.authService = new AuthService( this.authService = new AuthService(

View File

@@ -20,6 +20,8 @@ import {
PinServiceAbstraction, PinServiceAbstraction,
UserDecryptionOptionsService, UserDecryptionOptionsService,
SsoUrlService, SsoUrlService,
AuthRequestApiServiceAbstraction,
DefaultAuthRequestApiService,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
@@ -265,6 +267,7 @@ export class ServiceContainer {
devicesApiService: DevicesApiServiceAbstraction; devicesApiService: DevicesApiServiceAbstraction;
deviceTrustService: DeviceTrustServiceAbstraction; deviceTrustService: DeviceTrustServiceAbstraction;
authRequestService: AuthRequestService; authRequestService: AuthRequestService;
authRequestApiService: AuthRequestApiServiceAbstraction;
configApiService: ConfigApiServiceAbstraction; configApiService: ConfigApiServiceAbstraction;
configService: ConfigService; configService: ConfigService;
accountService: AccountService; accountService: AccountService;
@@ -616,14 +619,16 @@ export class ServiceContainer {
this.stateProvider, this.stateProvider,
); );
this.authRequestApiService = new DefaultAuthRequestApiService(this.apiService, this.logService);
this.authRequestService = new AuthRequestService( this.authRequestService = new AuthRequestService(
this.appIdService, this.appIdService,
this.accountService,
this.masterPasswordService, this.masterPasswordService,
this.keyService, this.keyService,
this.encryptService, this.encryptService,
this.apiService, this.apiService,
this.stateProvider, this.stateProvider,
this.authRequestApiService,
); );
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(

View File

@@ -16,13 +16,16 @@ import { filter, first, map, take } from "rxjs/operators";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; 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 { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 accountService: AccountService,
private cipherService: CipherService, private cipherService: CipherService,
private folderService: FolderService, private folderService: FolderService,
private authRequestService: AuthRequestServiceAbstraction,
private configService: ConfigService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -237,11 +242,30 @@ export class VaultComponent implements OnInit, OnDestroy {
this.searchBarService.setEnabled(true); this.searchBarService.setEnabled(true);
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
const authRequest = await this.apiService.getLastAuthRequest(); const browserLoginApprovalFeatureFlag = await firstValueFrom(
if (authRequest != null) { this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval),
this.messagingService.send("openLoginApproval", { );
notificationId: authRequest.id, 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)); this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));

View File

@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing"; import { RouterTestingModule } from "@angular/router/testing";
import { of, Subject } from "rxjs"; 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 { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view";
import { DeviceType } from "@bitwarden/common/enums"; import { DeviceType } from "@bitwarden/common/enums";
@@ -79,7 +79,7 @@ describe("DeviceManagementComponent", () => {
}, },
}, },
{ {
provide: AuthRequestApiService, provide: AuthRequestApiServiceAbstraction,
useValue: { useValue: {
getAuthRequest: jest.fn().mockResolvedValue(mockDeviceResponse), getAuthRequest: jest.fn().mockResolvedValue(mockDeviceResponse),
}, },

View File

@@ -4,7 +4,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { LoginApprovalComponent } from "@bitwarden/auth/angular"; 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 { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { import {
DevicePendingAuthRequest, DevicePendingAuthRequest,
@@ -61,7 +61,7 @@ export class DeviceManagementComponent {
private toastService: ToastService, private toastService: ToastService,
private validationService: ValidationService, private validationService: ValidationService,
private messageListener: MessageListener, private messageListener: MessageListener,
private authRequestApiService: AuthRequestApiService, private authRequestApiService: AuthRequestApiServiceAbstraction,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
) { ) {
void this.initializeDevices(); void this.initializeDevices();

View File

@@ -1,16 +1,19 @@
import { TestBed } from "@angular/core/testing"; 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 { import {
AuthRequestServiceAbstraction,
UserDecryptionOptions, UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; 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 { 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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { DeviceType } from "@bitwarden/common/enums"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
@@ -41,11 +44,15 @@ describe("VaultBannersService", () => {
[userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo, [userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo,
}); });
const devices$ = new BehaviorSubject<DeviceView[]>([]); const devices$ = new BehaviorSubject<DeviceView[]>([]);
const pendingAuthRequests$ = new BehaviorSubject<Array<AuthRequestResponse>>([]);
let configService: MockProxy<ConfigService>;
beforeEach(() => { beforeEach(() => {
lastSync$.next(new Date("2024-05-14")); lastSync$.next(new Date("2024-05-14"));
isSelfHost.mockClear(); isSelfHost.mockClear();
getEmailVerified.mockClear().mockResolvedValue(true); getEmailVerified.mockClear().mockResolvedValue(true);
configService = mock<ConfigService>();
configService.getFeatureFlag$.mockImplementation(() => of(true));
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
@@ -88,6 +95,14 @@ describe("VaultBannersService", () => {
provide: DevicesServiceAbstraction, provide: DevicesServiceAbstraction,
useValue: { getDevices$: () => devices$ }, useValue: { getDevices$: () => devices$ },
}, },
{
provide: AuthRequestServiceAbstraction,
useValue: { getPendingAuthRequests$: () => pendingAuthRequests$ },
},
{
provide: ConfigService,
useValue: configService,
},
], ],
}); });
}); });
@@ -286,31 +301,25 @@ describe("VaultBannersService", () => {
describe("PendingAuthRequest", () => { describe("PendingAuthRequest", () => {
const now = new Date(); const now = new Date();
let deviceResponse: DeviceResponse; let authRequestResponse: AuthRequestResponse;
beforeEach(() => { beforeEach(() => {
deviceResponse = new DeviceResponse({ authRequestResponse = new AuthRequestResponse({
Id: "device1", id: "authRequest1",
UserId: userId, deviceId: "device1",
Name: "Test Device", deviceName: "Test Device",
Identifier: "test-device", deviceType: DeviceType.Android,
Type: DeviceType.Android, creationDate: now.toISOString(),
CreationDate: now.toISOString(), requestApproved: null,
RevisionDate: now.toISOString(),
IsTrusted: false,
}); });
// Reset devices list, single user state, and active user state before each test // Reset devices list, single user state, and active user state before each test
devices$.next([]); pendingAuthRequests$.next([]);
fakeStateProvider.singleUser.states.clear(); fakeStateProvider.singleUser.states.clear();
fakeStateProvider.activeUser.states.clear(); fakeStateProvider.activeUser.states.clear();
}); });
it("shows pending auth request banner when there is a pending request", async () => { it("shows pending auth request banner when there is a pending request", async () => {
deviceResponse.devicePendingAuthRequest = { pendingAuthRequests$.next([new AuthRequestResponse(authRequestResponse)]);
id: "123",
creationDate: now.toISOString(),
};
devices$.next([new DeviceView(deviceResponse)]);
service = TestBed.inject(VaultBannersService); 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 () => { it("does not show pending auth request banner when there are no pending requests", async () => {
deviceResponse.devicePendingAuthRequest = null; pendingAuthRequests$.next([]);
devices$.next([new DeviceView(deviceResponse)]);
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);
@@ -327,11 +335,7 @@ describe("VaultBannersService", () => {
}); });
it("dismisses pending auth request banner", async () => { it("dismisses pending auth request banner", async () => {
deviceResponse.devicePendingAuthRequest = { pendingAuthRequests$.next([new AuthRequestResponse(authRequestResponse)]);
id: "123",
creationDate: now.toISOString(),
};
devices$.next([new DeviceView(deviceResponse)]);
service = TestBed.inject(VaultBannersService); service = TestBed.inject(VaultBannersService);

View File

@@ -1,10 +1,15 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; 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 { 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { import {
StateProvider, StateProvider,
@@ -66,20 +71,33 @@ export class VaultBannersService {
private syncService: SyncService, private syncService: SyncService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private devicesService: DevicesServiceAbstraction, private devicesService: DevicesServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private configService: ConfigService,
) {} ) {}
/** Returns true when the pending auth request banner should be shown */ /** Returns true when the pending auth request banner should be shown */
async shouldShowPendingAuthRequestBanner(userId: UserId): Promise<boolean> { async shouldShowPendingAuthRequestBanner(userId: UserId): Promise<boolean> {
const devices = await firstValueFrom(this.devicesService.getDevices$());
const hasPendingRequest = devices.some(
(device) => device.response?.devicePendingAuthRequest != null,
);
const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes( const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
VisibleVaultBanner.PendingAuthRequest, 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<boolean> { shouldShowPremiumBanner$(userId: UserId): Observable<boolean> {

View File

@@ -35,7 +35,7 @@ import {
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // 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 // eslint-disable-next-line no-restricted-imports
import { import {
AuthRequestApiService, AuthRequestApiServiceAbstraction,
AuthRequestService, AuthRequestService,
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
DefaultAuthRequestApiService, DefaultAuthRequestApiService,
@@ -1181,6 +1181,11 @@ const safeProviders: SafeProvider[] = [
useClass: DevicesServiceImplementation, useClass: DevicesServiceImplementation,
deps: [DevicesApiServiceAbstraction, AppIdServiceAbstraction], deps: [DevicesApiServiceAbstraction, AppIdServiceAbstraction],
}), }),
safeProvider({
provide: AuthRequestApiServiceAbstraction,
useClass: DefaultAuthRequestApiService,
deps: [ApiServiceAbstraction, LogService],
}),
safeProvider({ safeProvider({
provide: DeviceTrustServiceAbstraction, provide: DeviceTrustServiceAbstraction,
useClass: DeviceTrustService, useClass: DeviceTrustService,
@@ -1205,12 +1210,12 @@ const safeProviders: SafeProvider[] = [
useClass: AuthRequestService, useClass: AuthRequestService,
deps: [ deps: [
AppIdServiceAbstraction, AppIdServiceAbstraction,
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction, InternalMasterPasswordServiceAbstraction,
KeyService, KeyService,
EncryptService, EncryptService,
ApiServiceAbstraction, ApiServiceAbstraction,
StateProvider, StateProvider,
AuthRequestApiServiceAbstraction,
], ],
}), }),
safeProvider({ safeProvider({
@@ -1477,11 +1482,6 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultCipherAuthorizationService, useClass: DefaultCipherAuthorizationService,
deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction], deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction],
}), }),
safeProvider({
provide: AuthRequestApiService,
useClass: DefaultAuthRequestApiService,
deps: [ApiServiceAbstraction, LogService],
}),
safeProvider({ safeProvider({
provide: LoginApprovalComponentServiceAbstraction, provide: LoginApprovalComponentServiceAbstraction,
useClass: DefaultLoginApprovalComponentService, useClass: DefaultLoginApprovalComponentService,

View File

@@ -39,7 +39,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components"; import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; 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"; 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 // 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 accountService: AccountService,
private anonymousHubService: AnonymousHubService, private anonymousHubService: AnonymousHubService,
private appIdService: AppIdService, private appIdService: AppIdService,
private authRequestApiService: AuthRequestApiService, private authRequestApiService: AuthRequestApiServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction,
private authService: AuthService, private authService: AuthService,
private cryptoFunctionService: CryptoFunctionService, private cryptoFunctionService: CryptoFunctionService,

View File

@@ -1,7 +1,16 @@
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; 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<ListResponse<AuthRequestResponse>>;
export abstract class AuthRequestApiService {
/** /**
* Gets an auth request by its ID. * Gets an auth request by its ID.
* *

View File

@@ -41,6 +41,12 @@ export abstract class AuthRequestServiceAbstraction {
* @throws If `userId` is not provided. * @throws If `userId` is not provided.
*/ */
abstract clearAdminAuthRequest: (userId: UserId) => Promise<void>; abstract clearAdminAuthRequest: (userId: UserId) => Promise<void>;
/**
* 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<Array<AuthRequestResponse>>;
/** /**
* Approve or deny an auth request. * Approve or deny an auth request.
* @param approve True to approve, false to deny. * @param approve True to approve, false to deny.

View File

@@ -1,16 +1,23 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; 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 { 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( constructor(
private apiService: ApiService, private apiService: ApiService,
private logService: LogService, private logService: LogService,
) {} ) {}
async getPendingAuthRequests(): Promise<ListResponse<AuthRequestResponse>> {
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<AuthRequestResponse> { async getAuthRequest(requestId: string): Promise<AuthRequestResponse> {
try { try {
const path = `/auth-requests/${requestId}`; const path = `/auth-requests/${requestId}`;

View File

@@ -10,23 +10,23 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
import { DefaultAuthRequestApiService } from "./auth-request-api.service";
import { AuthRequestService } from "./auth-request.service"; import { AuthRequestService } from "./auth-request.service";
describe("AuthRequestService", () => { describe("AuthRequestService", () => {
let sut: AuthRequestService; let sut: AuthRequestService;
const stateProvider = mock<StateProvider>(); const stateProvider = mock<StateProvider>();
let accountService: FakeAccountService;
let masterPasswordService: FakeMasterPasswordService; let masterPasswordService: FakeMasterPasswordService;
const appIdService = mock<AppIdService>(); const appIdService = mock<AppIdService>();
const keyService = mock<KeyService>(); const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
const apiService = mock<ApiService>(); const apiService = mock<ApiService>();
const authRequestApiService = mock<DefaultAuthRequestApiService>();
let mockPrivateKey: Uint8Array; let mockPrivateKey: Uint8Array;
let mockPublicKey: Uint8Array; let mockPublicKey: Uint8Array;
@@ -34,17 +34,16 @@ describe("AuthRequestService", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
accountService = mockAccountServiceWith(mockUserId);
masterPasswordService = new FakeMasterPasswordService(); masterPasswordService = new FakeMasterPasswordService();
sut = new AuthRequestService( sut = new AuthRequestService(
appIdService, appIdService,
accountService,
masterPasswordService, masterPasswordService,
keyService, keyService,
encryptService, encryptService,
apiService, apiService,
stateProvider, stateProvider,
authRequestApiService,
); );
mockPrivateKey = new Uint8Array(64); mockPrivateKey = new Uint8Array(64);

View File

@@ -1,15 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Observable, Subject, firstValueFrom } from "rxjs"; import { Observable, Subject, defer, firstValueFrom, map } from "rxjs";
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; 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 { 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 { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; 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 { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
import { AuthRequestApiServiceAbstraction } from "../../abstractions/auth-request-api.service";
import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction"; import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction";
/** /**
@@ -49,12 +50,12 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
constructor( constructor(
private appIdService: AppIdService, private appIdService: AppIdService,
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction, private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private keyService: KeyService, private keyService: KeyService,
private encryptService: EncryptService, private encryptService: EncryptService,
private apiService: ApiService, private apiService: ApiService,
private stateProvider: StateProvider, private stateProvider: StateProvider,
private authRequestApiService: AuthRequestApiServiceAbstraction,
) { ) {
this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable();
this.adminLoginApproved$ = this.adminLoginApprovedSubject.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); await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, null, userId);
} }
/**
* @description Gets the list of all standard (not admin approval) pending AuthRequests.
*/
getPendingAuthRequests$(): Observable<Array<AuthRequestResponse>> {
return defer(() => this.authRequestApiService.getPendingAuthRequests()).pipe(
map((authRequestResponses: ListResponse<AuthRequestResponse>) => {
return authRequestResponses.data.map((authRequestResponse: AuthRequestResponse) => {
return new AuthRequestResponse(authRequestResponse);
});
}),
);
}
async approveOrDenyAuthRequest( async approveOrDenyAuthRequest(
approve: boolean, approve: boolean,
authRequest: AuthRequestResponse, authRequest: AuthRequestResponse,

View File

@@ -18,6 +18,7 @@ export class AuthRequestResponse extends BaseResponse {
responseDate?: string; responseDate?: string;
isAnswered: boolean; isAnswered: boolean;
isExpired: boolean; isExpired: boolean;
deviceId?: string; // could be null or empty
constructor(response: any) { constructor(response: any) {
super(response); super(response);
@@ -33,6 +34,7 @@ export class AuthRequestResponse extends BaseResponse {
this.creationDate = this.getResponseProperty("CreationDate"); this.creationDate = this.getResponseProperty("CreationDate");
this.requestApproved = this.getResponseProperty("RequestApproved"); this.requestApproved = this.getResponseProperty("RequestApproved");
this.responseDate = this.getResponseProperty("ResponseDate"); this.responseDate = this.getResponseProperty("ResponseDate");
this.deviceId = this.getResponseProperty("RequestDeviceId");
const requestDate = new Date(this.creationDate); const requestDate = new Date(this.creationDate);
const requestDateUTC = Date.UTC( const requestDateUTC = Date.UTC(

View File

@@ -19,6 +19,7 @@ export enum FeatureFlag {
PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor", PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor",
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor", PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence", PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence",
PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals",
/* Autofill */ /* Autofill */
BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain",
@@ -105,6 +106,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE, [FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE, [FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,
[FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE, [FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE,
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE,
/* Billing */ /* Billing */
[FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE,