1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

feat(prelogin): [Auth/PM-23801] Move Prelogin Request (#17080)

* feat(prelogin): [PM-23801] Move Prelogin Request - Initial implementation.

* test(prelogin): [PM-23801] Move Prelogin Request - Removed unneeded test.
This commit is contained in:
Patrick-Pimentel-Bitwarden
2025-11-10 10:54:25 -05:00
committed by GitHub
parent f7899991a0
commit 5aa6d38d80
9 changed files with 597 additions and 20 deletions

View File

@@ -1,4 +1,4 @@
<!--
<!--
# Table of Contents
This file contains a single consolidated template for all visual clients.

View File

@@ -0,0 +1,102 @@
import { FormBuilder } from "@angular/forms";
import { mock } from "jest-mock-extended";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ClientType } 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 { LoginComponent } from "./login.component";
describe("LoginComponent continue() integration", () => {
function createComponent({ flagEnabled }: { flagEnabled: boolean }) {
const activatedRoute: any = { queryParams: { subscribe: () => {} } };
const anonLayoutWrapperDataService: any = { setAnonLayoutWrapperData: () => {} };
const appIdService: any = {};
const broadcasterService: any = { subscribe: () => {}, unsubscribe: () => {} };
const destroyRef: any = {};
const devicesApiService: any = {};
const formBuilder = new FormBuilder();
const i18nService: any = { t: () => "" };
const loginEmailService: any = {
rememberedEmail$: { pipe: () => ({}) },
setLoginEmail: async () => {},
setRememberedEmailChoice: async () => {},
clearLoginEmail: async () => {},
};
const loginComponentService: any = {
showBackButton: () => {},
isLoginWithPasskeySupported: () => false,
redirectToSsoLogin: async () => {},
};
const loginStrategyService = mock<LoginStrategyServiceAbstraction>();
const messagingService: any = { send: () => {} };
const ngZone: any = { isStable: true, onStable: { pipe: () => ({ subscribe: () => {} }) } };
const passwordStrengthService: any = {};
const platformUtilsService = mock<PlatformUtilsService>();
platformUtilsService.getClientType.mockReturnValue(ClientType.Browser);
const policyService: any = { replace: async () => {}, evaluateMasterPassword: () => true };
const router: any = { navigate: async () => {}, navigateByUrl: async () => {} };
const toastService: any = { showToast: () => {} };
const logService: any = { error: () => {} };
const validationService: any = { showError: () => {} };
const loginSuccessHandlerService: any = { run: async () => {} };
const configService = mock<ConfigService>();
configService.getFeatureFlag.mockResolvedValue(flagEnabled);
const ssoLoginService: any = { ssoRequiredCache$: { pipe: () => ({}) } };
const environmentService: any = { environment$: { pipe: () => ({}) } };
const component = new LoginComponent(
activatedRoute,
anonLayoutWrapperDataService,
appIdService,
broadcasterService,
destroyRef,
devicesApiService,
formBuilder,
i18nService,
loginEmailService,
loginComponentService,
loginStrategyService,
messagingService,
ngZone,
passwordStrengthService,
platformUtilsService,
policyService,
router,
toastService,
logService,
validationService,
loginSuccessHandlerService,
configService,
ssoLoginService,
environmentService,
);
jest.spyOn(component as any, "toggleLoginUiState").mockResolvedValue(undefined);
return { component, loginStrategyService };
}
it("calls getPasswordPrelogin on continue when flag enabled and email valid", async () => {
const { component, loginStrategyService } = createComponent({ flagEnabled: true });
(component as any).formGroup.controls.email.setValue("user@example.com");
(component as any).formGroup.controls.rememberEmail.setValue(false);
(component as any).formGroup.controls.masterPassword.setValue("irrelevant");
await (component as any).continue();
expect(loginStrategyService.getPasswordPrelogin).toHaveBeenCalledWith("user@example.com");
});
it("does not call getPasswordPrelogin when flag disabled", async () => {
const { component, loginStrategyService } = createComponent({ flagEnabled: false });
(component as any).formGroup.controls.email.setValue("user@example.com");
(component as any).formGroup.controls.rememberEmail.setValue(false);
(component as any).formGroup.controls.masterPassword.setValue("irrelevant");
await (component as any).continue();
expect(loginStrategyService.getPasswordPrelogin).not.toHaveBeenCalled();
});
});

View File

@@ -550,6 +550,8 @@ export class LoginComponent implements OnInit, OnDestroy {
const isEmailValid = this.validateEmail();
if (isEmailValid) {
await this.makePasswordPreloginCall();
await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY);
}
}
@@ -652,6 +654,23 @@ export class LoginComponent implements OnInit, OnDestroy {
history.back();
}
private async makePasswordPreloginCall() {
// Prefetch prelogin KDF config when enabled
try {
const flagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM23801_PrefetchPasswordPrelogin,
);
if (flagEnabled) {
const email = this.formGroup.value.email;
if (email) {
void this.loginStrategyService.getPasswordPrelogin(email);
}
}
} catch (error) {
this.logService.error("Failed to prefetch prelogin data.", error);
}
}
/**
* Handle the popstate event to transition back to the email entry state when the back button is clicked.
* Also handles the case where the user clicks the forward button.

View File

@@ -65,7 +65,11 @@ export abstract class LoginStrategyServiceAbstraction {
/**
* Creates a master key from the provided master password and email.
*/
abstract makePreloginKey(masterPassword: string, email: string): Promise<MasterKey>;
abstract makePasswordPreLoginMasterKey(masterPassword: string, email: string): Promise<MasterKey>;
/**
* Prefetch and cache the KDF configuration for the given email. No-op if already in-flight or cached.
*/
abstract getPasswordPrelogin(email: string): Promise<void>;
/**
* Emits true if the authentication session has expired.
*/

View File

@@ -119,7 +119,7 @@ describe("PasswordLoginStrategy", () => {
sub: userId,
});
loginStrategyService.makePreloginKey.mockResolvedValue(masterKey);
loginStrategyService.makePasswordPreLoginMasterKey.mockResolvedValue(masterKey);
keyService.hashMasterKey
.calledWith(masterPassword, expect.anything(), undefined)

View File

@@ -79,7 +79,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
const { email, masterPassword, twoFactor } = credentials;
const data = new PasswordLoginStrategyData();
data.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email);
data.masterKey = await this.loginStrategyService.makePasswordPreLoginMasterKey(
masterPassword,
email,
);
data.userEnteredEmail = email;
// Hash the password early (before authentication) so we don't persist it in memory in plaintext

View File

@@ -37,7 +37,13 @@ import {
} from "@bitwarden/common/spec";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
import { KdfConfigService, KdfType, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
import {
Argon2KdfConfig,
KdfConfigService,
KdfType,
KeyService,
PBKDF2KdfConfig,
} from "@bitwarden/key-management";
import {
AuthRequestServiceAbstraction,
@@ -158,6 +164,321 @@ describe("LoginStrategyService", () => {
);
});
describe("PM23801_PrefetchPasswordPrelogin", () => {
describe("Flag On", () => {
it("prefetches and caches KDF, then makePrePasswordLoginMasterKey uses cached", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const email = "a@a.com";
apiService.postPrelogin.mockResolvedValue(
new PreloginResponse({
Kdf: KdfType.PBKDF2_SHA256,
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN,
}),
);
keyService.makeMasterKey.mockResolvedValue({} as any);
await sut.getPasswordPrelogin(email);
await sut.makePasswordPreLoginMasterKey("pw", email);
expect(apiService.postPrelogin).toHaveBeenCalledTimes(1);
const calls = keyService.makeMasterKey.mock.calls as any[];
expect(calls[0][2]).toBeInstanceOf(PBKDF2KdfConfig);
expect(keyService.makeMasterKey).toHaveBeenCalledWith(
"pw",
email.trim().toLowerCase(),
expect.any(PBKDF2KdfConfig),
);
});
it("awaits in-flight prelogin promise in makePrePasswordLoginMasterKey", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const email = "a@a.com";
let resolveFn: (v: any) => void;
const deferred = new Promise<PreloginResponse>((resolve) => (resolveFn = resolve));
apiService.postPrelogin.mockReturnValue(deferred as any);
keyService.makeMasterKey.mockResolvedValue({} as any);
void sut.getPasswordPrelogin(email);
const makeKeyPromise = sut.makePasswordPreLoginMasterKey("pw", email);
// Resolve after makePrePasswordLoginMasterKey has started awaiting
resolveFn!(
new PreloginResponse({
Kdf: KdfType.PBKDF2_SHA256,
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN,
}),
);
await makeKeyPromise;
expect(apiService.postPrelogin).toHaveBeenCalledTimes(1);
expect(keyService.makeMasterKey).toHaveBeenCalledWith(
"pw",
email,
expect.any(PBKDF2KdfConfig),
);
});
it("no cache and no in-flight request", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const email = "a@a.com";
apiService.postPrelogin.mockResolvedValue(
new PreloginResponse({
Kdf: KdfType.PBKDF2_SHA256,
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN,
}),
);
keyService.makeMasterKey.mockResolvedValue({} as any);
await sut.makePasswordPreLoginMasterKey("pw", email);
expect(apiService.postPrelogin).toHaveBeenCalledTimes(1);
expect(keyService.makeMasterKey).toHaveBeenCalledWith(
"pw",
email,
expect.any(PBKDF2KdfConfig),
);
});
it("falls back to API call when prefetched email differs", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const emailPrefetched = "a@a.com";
const emailUsed = "b@b.com";
// Prefetch for A
apiService.postPrelogin.mockResolvedValueOnce(
new PreloginResponse({
Kdf: KdfType.PBKDF2_SHA256,
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN,
}),
);
await sut.getPasswordPrelogin(emailPrefetched);
// makePrePasswordLoginMasterKey for B (forces new API call) -> Argon2
apiService.postPrelogin.mockResolvedValueOnce(
new PreloginResponse({
Kdf: KdfType.Argon2id,
KdfIterations: 2,
KdfMemory: 16,
KdfParallelism: 1,
}),
);
keyService.makeMasterKey.mockResolvedValue({} as any);
await sut.makePasswordPreLoginMasterKey("pw", emailUsed);
expect(apiService.postPrelogin).toHaveBeenCalledTimes(2);
const calls = keyService.makeMasterKey.mock.calls as any[];
expect(calls[calls.length - 1][2]).toBeInstanceOf(Argon2KdfConfig);
});
it("ignores stale prelogin resolution for older email (versioning)", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const emailA = "a@a.com";
const emailB = "b@b.com";
let resolveA!: (v: any) => void;
let resolveB!: (v: any) => void;
const deferredA = new Promise<PreloginResponse>((res) => (resolveA = res));
const deferredB = new Promise<PreloginResponse>((res) => (resolveB = res));
// First call returns A, second returns B
apiService.postPrelogin.mockImplementationOnce(() => deferredA as any);
apiService.postPrelogin.mockImplementationOnce(() => deferredB as any);
keyService.makeMasterKey.mockResolvedValue({} as any);
// Start A prefetch, then B prefetch (B supersedes A)
void sut.getPasswordPrelogin(emailA);
void sut.getPasswordPrelogin(emailB);
// Resolve A (stale) to PBKDF2, then B to Argon2
resolveA(
new PreloginResponse({
Kdf: KdfType.PBKDF2_SHA256,
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN,
}),
);
resolveB(
new PreloginResponse({
Kdf: KdfType.Argon2id,
KdfIterations: 2,
KdfMemory: 16,
KdfParallelism: 1,
}),
);
await sut.makePasswordPreLoginMasterKey("pwB", emailB);
// Ensure B's Argon2 config is used and stale A doesn't overwrite
const calls = keyService.makeMasterKey.mock.calls as any[];
const argB = calls.find((c) => c[0] === "pwB")[2];
expect(argB).toBeInstanceOf(Argon2KdfConfig);
});
it("handles concurrent getPasswordPrelogin calls for same email; uses latest result", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const email = "a@a.com";
let resolve1!: (v: any) => void;
let resolve2!: (v: any) => void;
const deferred1 = new Promise<PreloginResponse>((res) => (resolve1 = res));
const deferred2 = new Promise<PreloginResponse>((res) => (resolve2 = res));
apiService.postPrelogin.mockImplementationOnce(() => deferred1 as any);
apiService.postPrelogin.mockImplementationOnce(() => deferred2 as any);
keyService.makeMasterKey.mockResolvedValue({} as any);
void sut.getPasswordPrelogin(email);
void sut.getPasswordPrelogin(email);
// First resolves to PBKDF2, second resolves to Argon2 (latest wins)
resolve1(
new PreloginResponse({
Kdf: KdfType.PBKDF2_SHA256,
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN,
}),
);
resolve2(
new PreloginResponse({
Kdf: KdfType.Argon2id,
KdfIterations: 2,
KdfMemory: 16,
KdfParallelism: 1,
}),
);
await sut.makePasswordPreLoginMasterKey("pw", email);
expect(apiService.postPrelogin).toHaveBeenCalledTimes(2);
const calls = keyService.makeMasterKey.mock.calls as any[];
expect(calls[0][2]).toBeInstanceOf(Argon2KdfConfig);
});
it("does not throw when prefetch network error occurs; fallback works in makePrePasswordLoginMasterKey", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const email = "a@a.com";
// Prefetch throws non-404 error
const err: any = new Error("network");
err.statusCode = 500;
apiService.postPrelogin.mockRejectedValueOnce(err);
await expect(sut.getPasswordPrelogin(email)).resolves.toBeUndefined();
// makePrePasswordLoginMasterKey falls back to a new API call which succeeds
apiService.postPrelogin.mockResolvedValueOnce(
new PreloginResponse({
Kdf: KdfType.PBKDF2_SHA256,
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN,
}),
);
keyService.makeMasterKey.mockResolvedValue({} as any);
await sut.makePasswordPreLoginMasterKey("pw", email);
expect(apiService.postPrelogin).toHaveBeenCalledTimes(2);
const calls = keyService.makeMasterKey.mock.calls as any[];
expect(calls[0][2]).toBeInstanceOf(PBKDF2KdfConfig);
});
it("treats 404 as null prefetch and falls back in makePrePasswordLoginMasterKey", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const email = "a@a.com";
const notFound: any = new Error("not found");
notFound.statusCode = 404;
apiService.postPrelogin.mockRejectedValueOnce(notFound);
await sut.getPasswordPrelogin(email);
// Fallback call on makePrePasswordLoginMasterKey
apiService.postPrelogin.mockResolvedValueOnce(
new PreloginResponse({
Kdf: KdfType.Argon2id,
KdfIterations: 2,
KdfMemory: 16,
KdfParallelism: 1,
}),
);
keyService.makeMasterKey.mockResolvedValue({} as any);
await sut.makePasswordPreLoginMasterKey("pw", email);
expect(apiService.postPrelogin).toHaveBeenCalledTimes(2);
const calls = keyService.makeMasterKey.mock.calls as any[];
expect(calls[0][2]).toBeInstanceOf(Argon2KdfConfig);
});
it("awaits rejected current prelogin promise and then falls back in makePrePasswordLoginMasterKey", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const email = "a@a.com";
const err: any = new Error("network");
err.statusCode = 500;
let rejectFn!: (e: any) => void;
const deferred = new Promise<PreloginResponse>((_res, rej) => (rejectFn = rej));
apiService.postPrelogin.mockReturnValueOnce(deferred as any);
keyService.makeMasterKey.mockResolvedValue({} as any);
void sut.getPasswordPrelogin(email);
const makeKey = sut.makePasswordPreLoginMasterKey("pw", email);
rejectFn(err);
// Fallback call succeeds
apiService.postPrelogin.mockResolvedValueOnce(
new PreloginResponse({
Kdf: KdfType.PBKDF2_SHA256,
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN,
}),
);
await makeKey;
expect(apiService.postPrelogin).toHaveBeenCalledTimes(2);
const calls = keyService.makeMasterKey.mock.calls as any[];
expect(calls[0][2]).toBeInstanceOf(PBKDF2KdfConfig);
});
});
describe("Flag Off", () => {
// remove when pm-23801 feature flag comes out
it("uses legacy API path", async () => {
configService.getFeatureFlag.mockResolvedValue(false);
const email = "a@a.com";
// prefetch shouldn't affect behavior when flag off
apiService.postPrelogin.mockResolvedValue(
new PreloginResponse({
Kdf: KdfType.PBKDF2_SHA256,
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN,
}),
);
keyService.makeMasterKey.mockResolvedValue({} as any);
await sut.getPasswordPrelogin(email);
await sut.makePasswordPreLoginMasterKey("pw", email);
// Called twice: once for prefetch, once for legacy path in makePrePasswordLoginMasterKey
expect(apiService.postPrelogin).toHaveBeenCalledTimes(2);
expect(keyService.makeMasterKey).toHaveBeenCalledWith(
"pw",
email,
expect.any(PBKDF2KdfConfig),
);
});
});
});
it("should return an AuthResult on successful login", async () => {
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
apiService.postIdentityToken.mockResolvedValue(

View File

@@ -18,6 +18,7 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
@@ -92,6 +93,32 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
private authRequestPushNotificationState: GlobalState<string | null>;
private authenticationTimeoutSubject = new BehaviorSubject<boolean>(false);
// Prefetched password prelogin
//
// About versioning:
// Users can quickly change emails (e.g., continue with user1, go back, continue with user2)
// which triggers overlapping async prelogin requests. We use a monotonically increasing
// "version" to associate each prelogin attempt with the state at the time it was started.
// Only if BOTH the email and the version still match when the promise resolves do we commit
// the resulting KDF config or clear the in-flight promise. This prevents stale results from
// user1 overwriting user2's state in race conditions.
private passwordPrelogin: {
email: string | null;
kdfConfig: KdfConfig | null;
promise: Promise<KdfConfig | null> | null;
/**
* Version guard for prelogin attempts.
* Incremented at the start of getPasswordPrelogin for each new submission.
* Used to ignore stale async resolutions when email changes mid-flight.
*/
version: number;
} = {
email: null,
kdfConfig: null,
promise: null,
version: 0,
};
authenticationSessionTimeout$: Observable<boolean> =
this.authenticationTimeoutSubject.asObservable();
@@ -308,33 +335,106 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
}
}
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
async makePasswordPreLoginMasterKey(masterPassword: string, email: string): Promise<MasterKey> {
email = email.trim().toLowerCase();
let kdfConfig: KdfConfig | undefined;
if (await this.configService.getFeatureFlag(FeatureFlag.PM23801_PrefetchPasswordPrelogin)) {
let kdfConfig: KdfConfig | null = null;
if (this.passwordPrelogin.email === email) {
if (this.passwordPrelogin.kdfConfig) {
kdfConfig = this.passwordPrelogin.kdfConfig;
} else if (this.passwordPrelogin.promise != null) {
try {
await this.passwordPrelogin.promise;
} catch (error) {
this.logService.error(
"Failed to prefetch prelogin data, falling back to fetching now.",
error,
);
}
kdfConfig = this.passwordPrelogin.kdfConfig;
}
}
if (!kdfConfig) {
try {
const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email));
kdfConfig = this.buildKdfConfigFromPrelogin(preloginResponse);
} catch (e: any) {
if (e == null || e.statusCode !== 404) {
throw e;
}
}
}
if (!kdfConfig) {
throw new Error("KDF config is required");
}
kdfConfig.validateKdfConfigForPrelogin();
return await this.keyService.makeMasterKey(masterPassword, email, kdfConfig);
}
// Legacy behavior when flag is disabled
let legacyKdfConfig: KdfConfig | undefined;
try {
const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email));
if (preloginResponse != null) {
kdfConfig =
preloginResponse.kdf === KdfType.PBKDF2_SHA256
? new PBKDF2KdfConfig(preloginResponse.kdfIterations)
: new Argon2KdfConfig(
preloginResponse.kdfIterations,
preloginResponse.kdfMemory,
preloginResponse.kdfParallelism,
);
}
legacyKdfConfig = this.buildKdfConfigFromPrelogin(preloginResponse) ?? undefined;
} catch (e: any) {
if (e == null || e.statusCode !== 404) {
throw e;
}
}
if (!kdfConfig) {
if (!legacyKdfConfig) {
throw new Error("KDF config is required");
}
kdfConfig.validateKdfConfigForPrelogin();
legacyKdfConfig.validateKdfConfigForPrelogin();
return await this.keyService.makeMasterKey(masterPassword, email, legacyKdfConfig);
}
return await this.keyService.makeMasterKey(masterPassword, email, kdfConfig);
async getPasswordPrelogin(email: string): Promise<void> {
const normalizedEmail = email.trim().toLowerCase();
const version = ++this.passwordPrelogin.version;
this.passwordPrelogin.email = normalizedEmail;
this.passwordPrelogin.kdfConfig = null;
const promise: Promise<KdfConfig | null> = (async () => {
try {
const preloginResponse = await this.apiService.postPrelogin(
new PreloginRequest(normalizedEmail),
);
return this.buildKdfConfigFromPrelogin(preloginResponse);
} catch (e: any) {
if (e == null || e.statusCode !== 404) {
throw e;
}
return null;
}
})();
this.passwordPrelogin.promise = promise;
promise
.then((cfg) => {
// Only apply if still for the same email and same version
if (
this.passwordPrelogin.email === normalizedEmail &&
this.passwordPrelogin.version === version &&
cfg
) {
this.passwordPrelogin.kdfConfig = cfg;
}
})
.catch(() => {
// swallow; best-effort prefetch
})
.finally(() => {
if (
this.passwordPrelogin.email === normalizedEmail &&
this.passwordPrelogin.version === version
) {
this.passwordPrelogin.promise = null;
}
});
}
private async clearCache(): Promise<void> {
@@ -342,6 +442,12 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
await this.loginStrategyCacheState.update((_) => null);
this.authenticationTimeoutSubject.next(false);
await this.clearSessionTimeout();
// Increment to invalidate any in-flight requests
this.passwordPrelogin.version++;
this.passwordPrelogin.email = null;
this.passwordPrelogin.kdfConfig = null;
this.passwordPrelogin.promise = null;
}
private async startSessionTimeout(): Promise<void> {
@@ -449,4 +555,24 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
}),
);
}
private buildKdfConfigFromPrelogin(
preloginResponse: {
kdf: KdfType;
kdfIterations: number;
kdfMemory?: number;
kdfParallelism?: number;
} | null,
): KdfConfig | null {
if (preloginResponse == null) {
return null;
}
return preloginResponse.kdf === KdfType.PBKDF2_SHA256
? new PBKDF2KdfConfig(preloginResponse.kdfIterations)
: new Argon2KdfConfig(
preloginResponse.kdfIterations,
preloginResponse.kdfMemory,
preloginResponse.kdfParallelism,
);
}
}

View File

@@ -16,6 +16,7 @@ export enum FeatureFlag {
/* Auth */
PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods",
PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin",
/* Autofill */
MacOsNativeCredentialSync = "macos-native-credential-sync",
@@ -111,6 +112,7 @@ export const DefaultFeatureFlagValue = {
/* Auth */
[FeatureFlag.PM22110_DisableAlternateLoginMethods]: FALSE,
[FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE,
/* Billing */
[FeatureFlag.TrialPaymentOptional]: FALSE,