1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[PM-7564] Move 2fa and login strategy service to popup and add state providers to 2fa service (#8820)

* remove 2fa from main.background

* remove login strategy service from main.background

* move 2fa and login strategy service to popup, init in browser

* add state providers to 2fa service
- add deserializer helpers

* use key definitions for global state

* fix calls to 2fa service

* remove extra await

* add delay to wait for active account emission in popup

* add and fix tests

* fix cli

* really fix cli

* remove timeout and wait for active account

* verify expected user is active account

* fix tests

* address feedback
This commit is contained in:
Jake Fink
2024-04-25 16:45:23 -04:00
committed by GitHub
parent cbf7c292f3
commit 8afe915be1
27 changed files with 217 additions and 152 deletions

View File

@@ -86,7 +86,9 @@ describe("AuthRequestLoginStrategy", () => {
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeAccessToken.mockResolvedValue({});
tokenService.decodeAccessToken.mockResolvedValue({
sub: mockUserId,
});
authRequestLoginStrategy = new AuthRequestLoginStrategy(
cache,

View File

@@ -25,11 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
Account,
AccountProfile,
AccountKeys,
} from "@bitwarden/common/platform/models/domain/account";
import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
@@ -214,7 +210,6 @@ describe("LoginStrategy", () => {
email: email,
},
},
keys: new AccountKeys(),
}),
);
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
@@ -223,6 +218,21 @@ describe("LoginStrategy", () => {
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
});
it("throws if active account isn't found after being initialized", async () => {
const idTokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
const mockVaultTimeout = 1000;
stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction);
stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout);
accountService.activeAccountSubject.next(null);
await expect(async () => await passwordLoginStrategy.logIn(credentials)).rejects.toThrow();
});
it("builds AuthResult", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.forcePasswordReset = true;
@@ -306,8 +316,10 @@ describe("LoginStrategy", () => {
expect(tokenService.clearTwoFactorToken).toHaveBeenCalled();
const expected = new AuthResult();
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
expected.twoFactorProviders.set(0, null);
expected.twoFactorProviders = { 0: null } as Record<
TwoFactorProviderType,
Record<string, string>
>;
expect(result).toEqual(expected);
});
@@ -336,8 +348,9 @@ describe("LoginStrategy", () => {
expect(messagingService.send).not.toHaveBeenCalled();
const expected = new AuthResult();
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
expected.twoFactorProviders.set(1, { Email: "k***@bitwarden.com" });
expected.twoFactorProviders = {
[TwoFactorProviderType.Email]: { Email: "k***@bitwarden.com" },
};
expected.email = userEmail;
expected.ssoEmail2FaSessionToken = ssoEmail2FaSessionToken;

View File

@@ -1,4 +1,4 @@
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -101,7 +101,7 @@ export abstract class LoginStrategy {
}
protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> {
this.twoFactorService.clearSelectedProvider();
await this.twoFactorService.clearSelectedProvider();
const tokenRequest = this.cache.value.tokenRequest;
const response = await this.apiService.postIdentityToken(tokenRequest);
@@ -159,12 +159,12 @@ export abstract class LoginStrategy {
* It also sets the access token and refresh token in the token service.
*
* @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token.
* @returns {Promise<void>} - A promise that resolves when the account information has been successfully saved.
* @returns {Promise<UserId>} - A promise that resolves the the UserId when the account information has been successfully saved.
*/
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> {
const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken);
const userId = accountInformation.sub;
const userId = accountInformation.sub as UserId;
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId });
const vaultTimeout = await this.stateService.getVaultTimeout({ userId });
@@ -191,6 +191,8 @@ export abstract class LoginStrategy {
}),
);
await this.verifyAccountAdded(userId);
await this.userDecryptionOptionsService.setUserDecryptionOptions(
UserDecryptionOptions.fromResponse(tokenResponse),
);
@@ -207,7 +209,7 @@ export abstract class LoginStrategy {
);
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
return userId as UserId;
return userId;
}
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
@@ -284,7 +286,7 @@ export abstract class LoginStrategy {
const result = new AuthResult();
result.twoFactorProviders = response.twoFactorProviders2;
this.twoFactorService.setProviders(response);
await this.twoFactorService.setProviders(response);
this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null });
result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken;
result.email = response.email;
@@ -306,4 +308,24 @@ export abstract class LoginStrategy {
result.captchaSiteKey = response.siteKey;
return result;
}
/**
* Verifies that the active account is set after initialization.
* Note: In browser there is a slight delay between when active account emits in background,
* and when it emits in foreground. We're giving the foreground 1 second to catch up.
* If nothing is emitted, we throw an error.
*/
private async verifyAccountAdded(expectedUserId: UserId) {
await firstValueFrom(
this.accountService.activeAccount$.pipe(
filter((account) => account?.id === expectedUserId),
timeout({
first: 1000,
with: () => {
throw new Error("Expected user never made active user after initialization.");
},
}),
),
);
}
}

View File

@@ -99,7 +99,9 @@ describe("PasswordLoginStrategy", () => {
kdfConfigService = mock<KdfConfigService>();
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeAccessToken.mockResolvedValue({});
tokenService.decodeAccessToken.mockResolvedValue({
sub: userId,
});
loginStrategyService.makePreloginKey.mockResolvedValue(masterKey);

View File

@@ -92,7 +92,9 @@ describe("SsoLoginStrategy", () => {
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeAccessToken.mockResolvedValue({});
tokenService.decodeAccessToken.mockResolvedValue({
sub: userId,
});
ssoLoginStrategy = new SsoLoginStrategy(
null,

View File

@@ -82,7 +82,9 @@ describe("UserApiLoginStrategy", () => {
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.getTwoFactorToken.mockResolvedValue(null);
tokenService.decodeAccessToken.mockResolvedValue({});
tokenService.decodeAccessToken.mockResolvedValue({
sub: userId,
});
apiLogInStrategy = new UserApiLoginStrategy(
cache,

View File

@@ -18,7 +18,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { FakeAccountService } from "@bitwarden/common/spec";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { PrfKey, UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
@@ -49,6 +50,7 @@ describe("WebAuthnLoginStrategy", () => {
const token = "mockToken";
const deviceId = Utils.newGuid();
const userId = Utils.newGuid() as UserId;
let webAuthnCredentials!: WebAuthnLoginCredentials;
@@ -69,7 +71,7 @@ describe("WebAuthnLoginStrategy", () => {
beforeEach(() => {
jest.clearAllMocks();
accountService = new FakeAccountService(null);
accountService = mockAccountServiceWith(userId);
masterPasswordService = new FakeMasterPasswordService();
cryptoService = mock<CryptoService>();
@@ -87,7 +89,9 @@ describe("WebAuthnLoginStrategy", () => {
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeAccessToken.mockResolvedValue({});
tokenService.decodeAccessToken.mockResolvedValue({
sub: userId,
});
webAuthnLoginStrategy = new WebAuthnLoginStrategy(
cache,