mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ListResponse<AuthRequestResponse>>;
|
||||
|
||||
export abstract class AuthRequestApiService {
|
||||
/**
|
||||
* Gets an auth request by its ID.
|
||||
*
|
||||
|
||||
@@ -41,6 +41,12 @@ export abstract class AuthRequestServiceAbstraction {
|
||||
* @throws If `userId` is not provided.
|
||||
*/
|
||||
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.
|
||||
* @param approve True to approve, false to deny.
|
||||
|
||||
@@ -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<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> {
|
||||
try {
|
||||
const path = `/auth-requests/${requestId}`;
|
||||
|
||||
@@ -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<StateProvider>();
|
||||
let accountService: FakeAccountService;
|
||||
let masterPasswordService: FakeMasterPasswordService;
|
||||
const appIdService = mock<AppIdService>();
|
||||
const keyService = mock<KeyService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const apiService = mock<ApiService>();
|
||||
const authRequestApiService = mock<DefaultAuthRequestApiService>();
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<Array<AuthRequestResponse>> {
|
||||
return defer(() => this.authRequestApiService.getPendingAuthRequests()).pipe(
|
||||
map((authRequestResponses: ListResponse<AuthRequestResponse>) => {
|
||||
return authRequestResponses.data.map((authRequestResponse: AuthRequestResponse) => {
|
||||
return new AuthRequestResponse(authRequestResponse);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async approveOrDenyAuthRequest(
|
||||
approve: boolean,
|
||||
authRequest: AuthRequestResponse,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user