diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html index 22cf8320036..d6b91b960b0 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html @@ -26,7 +26,7 @@ block buttonType="secondary" class="tw-mt-4" - (click)="startStandardAuthRequestLogin()" + (click)="startStandardAuthRequestLogin(true)" > {{ "resendNotification" | i18n }} 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 266ee3c4acc..bb2822d67e9 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 @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -24,10 +22,13 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -40,6 +41,7 @@ import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { AuthRequestApiService } from "../../common/abstractions/auth-request-api.service"; +import { LoginViaAuthRequestCacheService } from "../../common/services/auth-request/default-login-via-auth-request-cache.service"; enum Flow { StandardAuthRequest, // when user clicks "Login with device" from /login or "Approve from your other device" from /login-initiated @@ -57,23 +59,26 @@ const matchOptions: IsActiveMatchOptions = { standalone: true, templateUrl: "./login-via-auth-request.component.html", imports: [ButtonModule, CommonModule, JslibModule, LinkModule, RouterModule], + providers: [{ provide: LoginViaAuthRequestCacheService }], }) export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { - private authRequest: AuthRequest; - private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array }; - private authStatus: AuthenticationStatus; + private authRequest: AuthRequest | undefined = undefined; + private authRequestKeyPair: + | { publicKey: Uint8Array | undefined; privateKey: Uint8Array | undefined } + | undefined = undefined; + private authStatus: AuthenticationStatus | undefined = undefined; private showResendNotificationTimeoutSeconds = 12; protected backToRoute = "/login"; protected clientType: ClientType; protected ClientType = ClientType; - protected email: string; - protected fingerprintPhrase: string; + protected email: string | undefined = undefined; + protected fingerprintPhrase: string | undefined = undefined; protected showResendNotification = false; protected Flow = Flow; protected flow = Flow.StandardAuthRequest; - protected webVaultUrl: string; - protected deviceManagementUrl: string; + protected webVaultUrl: string | undefined = undefined; + protected deviceManagementUrl: string | undefined; constructor( private accountService: AccountService, @@ -95,6 +100,8 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private toastService: ToastService, private validationService: ValidationService, private loginSuccessHandlerService: LoginSuccessHandlerService, + private loginViaAuthRequestCacheService: LoginViaAuthRequestCacheService, + private configService: ConfigService, ) { this.clientType = this.platformUtilsService.getClientType(); @@ -124,6 +131,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { async ngOnInit(): Promise { // Get the authStatus early because we use it in both flows this.authStatus = await firstValueFrom(this.authService.activeAccountStatus$); + await this.loginViaAuthRequestCacheService.init(); const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked; @@ -133,7 +141,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { /** * The LoginViaAuthRequestComponent handles both the `login-with-device` and - * the `admin-approval-requested` routes. Therefore we check the route to determine + * the `admin-approval-requested` routes. Therefore, we check the route to determine * which flow to initialize. */ if (this.router.isActive("admin-approval-requested", matchOptions)) { @@ -159,7 +167,14 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { // We only allow a single admin approval request to be active at a time // so we must check state to see if we have an existing one or not - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (!userId) { + this.logService.error( + "Not able to get a user id from the account service active account observable.", + ); + return; + } + const existingAdminAuthRequest = await this.authRequestService.getAdminAuthRequest(userId); if (existingAdminAuthRequest) { @@ -172,7 +187,9 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private async initStandardAuthRequestFlow(): Promise { this.flow = Flow.StandardAuthRequest; - this.email = await firstValueFrom(this.loginEmailService.loginEmail$); + this.email = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.email)), + ); if (!this.email) { await this.handleMissingEmail(); @@ -185,7 +202,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private async handleMissingEmail(): Promise { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("userEmailMissing"), }); @@ -194,21 +210,41 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { async ngOnDestroy(): Promise { await this.anonymousHubService.stopHubConnection(); + + this.loginViaAuthRequestCacheService.clearCacheLoginView(); } private async startAdminAuthRequestLogin(): Promise { try { await this.buildAuthRequest(AuthRequestType.AdminApproval); + if (!this.authRequest) { + this.logService.error("Auth request failed to build."); + return; + } + + if (!this.authRequestKeyPair) { + this.logService.error("Key pairs failed to initialize from buildAuthRequest."); + return; + } + const authRequestResponse = await this.authRequestApiService.postAdminAuthRequest( - this.authRequest, + this.authRequest as AuthRequest, ); const adminAuthReqStorable = new AdminAuthRequestStorable({ id: authRequestResponse.id, privateKey: this.authRequestKeyPair.privateKey, }); - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + + if (!userId) { + this.logService.error( + "Not able to get a user id from the account service active account observable.", + ); + return; + } + await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId); if (authRequestResponse.id) { @@ -219,21 +255,104 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { } } - protected async startStandardAuthRequestLogin(): Promise { + protected async startStandardAuthRequestLogin( + clearCachedRequest: boolean = false, + ): Promise { this.showResendNotification = false; - try { - await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock); - - const authRequestResponse = await this.authRequestApiService.postAuthRequest( - this.authRequest, - ); - - if (authRequestResponse.id) { - await this.anonymousHubService.createHubConnection(authRequestResponse.id); + if (await this.configService.getFeatureFlag(FeatureFlag.PM9112_DeviceApprovalPersistence)) { + // Used for manually refreshing the auth request when clicking the resend auth request + // on the ui. + if (clearCachedRequest) { + this.loginViaAuthRequestCacheService.clearCacheLoginView(); + } + + try { + const loginAuthRequestView: LoginViaAuthRequestView | null = + this.loginViaAuthRequestCacheService.getCachedLoginViaAuthRequestView(); + + if (!loginAuthRequestView) { + await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock); + + // I tried several ways to get the IDE/linter to play nice with checking for null values + // in less code / more efficiently, but it struggles to identify code paths that + // are more complicated than this. + if (!this.authRequest) { + this.logService.error("AuthRequest failed to initialize from buildAuthRequest."); + return; + } + + if (!this.fingerprintPhrase) { + this.logService.error("FingerprintPhrase failed to initialize from buildAuthRequest."); + return; + } + + if (!this.authRequestKeyPair) { + this.logService.error("KeyPair failed to initialize from buildAuthRequest."); + return; + } + + const authRequestResponse: AuthRequestResponse = + await this.authRequestApiService.postAuthRequest(this.authRequest); + + this.loginViaAuthRequestCacheService.cacheLoginView( + this.authRequest, + authRequestResponse, + this.fingerprintPhrase, + this.authRequestKeyPair, + ); + + if (authRequestResponse.id) { + await this.anonymousHubService.createHubConnection(authRequestResponse.id); + } + } else { + // Grab the cached information and store it back in component state. + // We don't need the public key for handling the authentication request because + // the verifyAndHandleApprovedAuthReq function will receive the public key back + // from the looked up auth request and all we need is to make sure that + // we can use the cached private key that is associated with it. + this.authRequest = loginAuthRequestView.authRequest; + this.fingerprintPhrase = loginAuthRequestView.fingerprintPhrase; + this.authRequestKeyPair = { + privateKey: loginAuthRequestView.privateKey + ? Utils.fromB64ToArray(loginAuthRequestView.privateKey) + : undefined, + publicKey: undefined, + }; + + if (!loginAuthRequestView.authRequestResponse) { + this.logService.error("No cached auth request response."); + return; + } + + if (loginAuthRequestView.authRequestResponse.id) { + await this.anonymousHubService.createHubConnection( + loginAuthRequestView.authRequestResponse.id, + ); + } + } + } catch (e) { + this.logService.error(e); + } + } else { + try { + await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock); + + if (!this.authRequest) { + this.logService.error("No auth request found."); + return; + } + + const authRequestResponse = await this.authRequestApiService.postAuthRequest( + this.authRequest, + ); + + if (authRequestResponse.id) { + await this.anonymousHubService.createHubConnection(authRequestResponse.id); + } + } catch (e) { + this.logService.error(e); } - } catch (e) { - this.logService.error(e); } setTimeout(() => { @@ -250,12 +369,23 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { }; const deviceIdentifier = await this.appIdService.getAppId(); + + if (!this.authRequestKeyPair.publicKey) { + this.logService.error("AuthRequest public key not set to value in building auth request."); + return; + } + const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey); const accessCode = await this.passwordGenerationService.generatePassword({ type: "password", length: 25, }); + if (!this.email) { + this.logService.error("Email not defined when building auth request."); + return; + } + this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase( this.email, this.authRequestKeyPair.publicKey, @@ -288,6 +418,8 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) { return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); } + this.logService.error(error); + return; } // Request doesn't exist anymore @@ -300,6 +432,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey( adminAuthRequestStorable.privateKey, ); + + if (!this.email) { + this.logService.error("Email not defined when handling an existing an admin auth request."); + return; + } + this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase( this.email, derivedPublicKeyArrayBuffer, @@ -319,9 +457,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { ); } - // Request still pending response from admin - // set keypair and create hub connection so that any approvals will be received via push notification - this.authRequestKeyPair = { privateKey: adminAuthRequestStorable.privateKey, publicKey: null }; + // Request still pending response from admin set keypair and create hub connection + // so that any approvals will be received via push notification + this.authRequestKeyPair = { + privateKey: adminAuthRequestStorable.privateKey, + publicKey: undefined, + }; await this.anonymousHubService.createHubConnection(adminAuthRequestStorable.id); } @@ -403,6 +544,11 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { await this.handleAuthenticatedFlows(authRequestResponse); } } else { + if (!this.authRequest) { + this.logService.error("No auth request defined when handling approved auth request."); + return; + } + // Get the auth request from the server // User is unauthenticated, therefore the endpoint requires an access code for user verification. const authRequestResponse = await this.authRequestApiService.getAuthResponse( @@ -423,11 +569,26 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { } this.logService.error(error); + } finally { + // Manually clean out the cache to make sure sensitive + // data does not persist longer than it needs to. + this.loginViaAuthRequestCacheService.clearCacheLoginView(); } } private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) { - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (!userId) { + this.logService.error( + "Not able to get a user id from the account service active account observable.", + ); + return; + } + + if (!this.authRequestKeyPair || !this.authRequestKeyPair.privateKey) { + this.logService.error("No private key set when handling the authenticated flows."); + return; + } await this.decryptViaApprovedAuthRequest( authRequestResponse, @@ -445,6 +606,11 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { authRequestResponse, ); + if (!authRequestLoginCredentials) { + this.logService.error("Didn't set up auth request login credentials properly."); + return; + } + // Note: keys are set by AuthRequestLoginStrategy success handling const authResult = await this.loginStrategyService.logIn(authRequestLoginCredentials); @@ -463,7 +629,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { * - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)] * - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey) */ - if (authRequestResponse.masterPasswordHash) { // ...in Standard Auth Request Flow 3 await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash( @@ -486,13 +651,17 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t("loginApproved"), }); // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (!activeAccount) { + this.logService.error("No active account defined from the account service."); + return; + } + await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); await this.handleSuccessfulLoginNavigation(userId); @@ -508,7 +677,24 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private async buildAuthRequestLoginCredentials( requestId: string, authRequestResponse: AuthRequestResponse, - ): Promise { + ): Promise { + if (!this.authRequestKeyPair || !this.authRequestKeyPair.privateKey) { + this.logService.error("No private key set when building auth request login credentials."); + return; + } + + if (!this.email) { + this.logService.error("Email not defined."); + return; + } + + if (!this.authRequest) { + this.logService.error( + "AuthRequest not defined when building auth request login credentials.", + ); + return; + } + /** * See verifyAndHandleApprovedAuthReq() for flow details. * @@ -516,7 +702,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { * - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)] * - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey) */ - if (authRequestResponse.masterPasswordHash) { // ...in Standard Auth Request Flow 1 const { masterKey, masterKeyHash } = diff --git a/libs/auth/src/common/models/domain/login-credentials.ts b/libs/auth/src/common/models/domain/login-credentials.ts index 72cc7413bec..cc21e5b2505 100644 --- a/libs/auth/src/common/models/domain/login-credentials.ts +++ b/libs/auth/src/common/models/domain/login-credentials.ts @@ -53,9 +53,9 @@ export class AuthRequestLoginCredentials { public email: string, public accessCode: string, public authRequestId: string, - public decryptedUserKey: UserKey, - public decryptedMasterKey: MasterKey, - public decryptedMasterKeyHash: string, + public decryptedUserKey: UserKey | null, + public decryptedMasterKey: MasterKey | null, + public decryptedMasterKeyHash: string | null, public twoFactor?: TokenTwoFactorRequest, ) {} diff --git a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts new file mode 100644 index 00000000000..82ac0f1006d --- /dev/null +++ b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts @@ -0,0 +1,111 @@ +import { signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type"; +import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; +import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { LoginViaAuthRequestCacheService } from "./default-login-via-auth-request-cache.service"; + +describe("LoginViaAuthRequestCache", () => { + let service: LoginViaAuthRequestCacheService; + let testBed: TestBed; + + const cacheSignal = signal(null); + const getCacheSignal = jest.fn().mockReturnValue(cacheSignal); + const getFeatureFlag = jest.fn().mockResolvedValue(false); + const cacheSetMock = jest.spyOn(cacheSignal, "set"); + + beforeEach(() => { + getCacheSignal.mockClear(); + getFeatureFlag.mockClear(); + cacheSetMock.mockClear(); + + testBed = TestBed.configureTestingModule({ + providers: [ + { provide: ViewCacheService, useValue: { signal: getCacheSignal } }, + { provide: ConfigService, useValue: { getFeatureFlag } }, + LoginViaAuthRequestCacheService, + ], + }); + }); + + describe("feature enabled", () => { + beforeEach(() => { + getFeatureFlag.mockResolvedValue(true); + }); + + it("`getCachedLoginViaAuthRequestView` returns the cached data", async () => { + cacheSignal.set({ ...buildAuthenticMockAuthView() }); + service = testBed.inject(LoginViaAuthRequestCacheService); + await service.init(); + + expect(service.getCachedLoginViaAuthRequestView()).toEqual({ + ...buildAuthenticMockAuthView(), + }); + }); + + it("updates the signal value", async () => { + service = testBed.inject(LoginViaAuthRequestCacheService); + await service.init(); + + const parameters = buildAuthenticMockAuthView(); + + service.cacheLoginView( + parameters.authRequest, + parameters.authRequestResponse, + parameters.fingerprintPhrase, + { publicKey: new Uint8Array(), privateKey: new Uint8Array() }, + ); + + expect(cacheSignal.set).toHaveBeenCalledWith(parameters); + }); + }); + + describe("feature disabled", () => { + beforeEach(async () => { + cacheSignal.set({ ...buildAuthenticMockAuthView() } as LoginViaAuthRequestView); + getFeatureFlag.mockResolvedValue(false); + cacheSetMock.mockClear(); + + service = testBed.inject(LoginViaAuthRequestCacheService); + await service.init(); + }); + + it("`getCachedCipherView` returns null", () => { + expect(service.getCachedLoginViaAuthRequestView()).toBeNull(); + }); + + it("does not update the signal value", () => { + const params = buildAuthenticMockAuthView(); + + service.cacheLoginView( + params.authRequest, + params.authRequestResponse, + params.fingerprintPhrase, + { publicKey: new Uint8Array(), privateKey: new Uint8Array() }, + ); + + expect(cacheSignal.set).not.toHaveBeenCalled(); + }); + }); + + const buildAuthenticMockAuthView = () => { + return { + fingerprintPhrase: "", + privateKey: "", + publicKey: "", + authRequest: new AuthRequest( + "test@gmail.com", + "deviceIdentifier", + "publicKey", + AuthRequestType.Unlock, + "accessCode", + ), + authRequestResponse: new AuthRequestResponse({}), + }; + }; +}); diff --git a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts new file mode 100644 index 00000000000..30ba8879546 --- /dev/null +++ b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts @@ -0,0 +1,88 @@ +import { inject, Injectable, WritableSignal } from "@angular/core"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; +import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +const LOGIN_VIA_AUTH_CACHE_KEY = "login-via-auth-request-form-cache"; + +/** + * This is a cache service used for the login via auth request component. + * + * There is sensitive information stored temporarily here. Cache will be cleared + * after 2 minutes. + */ +@Injectable() +export class LoginViaAuthRequestCacheService { + private viewCacheService: ViewCacheService = inject(ViewCacheService); + private configService: ConfigService = inject(ConfigService); + + /** True when the `PM9112_DeviceApproval` flag is enabled */ + private featureEnabled: boolean = false; + + private defaultLoginViaAuthRequestCache: WritableSignal = + this.viewCacheService.signal({ + key: LOGIN_VIA_AUTH_CACHE_KEY, + initialValue: null, + deserializer: LoginViaAuthRequestView.fromJSON, + }); + + constructor() {} + + /** + * Must be called once before interacting with the cached data, otherwise methods will be noop. + */ + async init() { + this.featureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM9112_DeviceApprovalPersistence, + ); + } + + /** + * Update the cache with the new LoginView. + */ + cacheLoginView( + authRequest: AuthRequest, + authRequestResponse: AuthRequestResponse, + fingerprintPhrase: string, + keys: { privateKey: Uint8Array | undefined; publicKey: Uint8Array | undefined }, + ): void { + if (!this.featureEnabled) { + return; + } + + // When the keys get stored they should be converted to a B64 string to ensure + // data can be properly formed when json-ified. If not done, they are not stored properly and + // will not be parsable by the cryptography library after coming out of storage. + this.defaultLoginViaAuthRequestCache.set({ + authRequest, + authRequestResponse, + fingerprintPhrase, + privateKey: keys.privateKey ? Utils.fromBufferToB64(keys.privateKey.buffer) : undefined, + publicKey: keys.publicKey ? Utils.fromBufferToB64(keys.publicKey.buffer) : undefined, + } as LoginViaAuthRequestView); + } + + clearCacheLoginView(): void { + if (!this.featureEnabled) { + return; + } + + this.defaultLoginViaAuthRequestCache.set(null); + } + + /** + * Returns the cached LoginViaAuthRequestView when available. + */ + getCachedLoginViaAuthRequestView(): LoginViaAuthRequestView | null { + if (!this.featureEnabled) { + return null; + } + + return this.defaultLoginViaAuthRequestCache(); + } +} diff --git a/libs/common/src/auth/models/view/login-via-auth-request.view.ts b/libs/common/src/auth/models/view/login-via-auth-request.view.ts new file mode 100644 index 00000000000..0691b8efd86 --- /dev/null +++ b/libs/common/src/auth/models/view/login-via-auth-request.view.ts @@ -0,0 +1,17 @@ +import { Jsonify } from "type-fest"; + +import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; +import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { View } from "@bitwarden/common/models/view/view"; + +export class LoginViaAuthRequestView implements View { + authRequest: AuthRequest | undefined = undefined; + authRequestResponse: AuthRequestResponse | undefined = undefined; + fingerprintPhrase: string | undefined = undefined; + privateKey: string | undefined = undefined; + publicKey: string | undefined = undefined; + + static fromJSON(obj: Partial>): LoginViaAuthRequestView { + return Object.assign(new LoginViaAuthRequestView(), obj); + } +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 280e8d8a989..d35a1854653 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -36,6 +36,9 @@ export enum FeatureFlag { VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", + /* Auth */ + PM9112_DeviceApprovalPersistence = "pm-9112-device-approval-persistence", + PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", CipherKeyEncryption = "cipher-key-encryption", @@ -93,6 +96,9 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, + /* Auth */ + [FeatureFlag.PM9112_DeviceApprovalPersistence]: FALSE, + [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE,