1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

[PM-5264] Implement StateProvider in LoginEmailService (#7662)

* setup StateProvider in LoginService

* replace implementations

* replace implementation

* remove stateService

* change storage location for web to 'disk-local'

* implement migrate() method of Migrator

* add RememberedEmailMigrator to migrate.ts

* add rollback

* add tests

* replace implementation

* replace implementation

* add StateProvider to Desktop services

* rename LoginService to RememberEmailService

* update state definition

* rename file

* rename to storedEmail

* rename service to EmailService to avoid confusion

* add jsdocs

* refactor login.component.ts

* fix typos

* fix test

* rename to LoginEmailService

* update factory

* more renaming

* remove duplicate logic and rename method

* convert storedEmail to observable

* refactor to remove setStoredEmail() method

* move service to libs/auth/common

* address floating promises

* remove comment

* remove unnecessary deps in service registration
This commit is contained in:
rr-bw
2024-03-30 11:00:27 -07:00
committed by GitHub
parent 77cfa8a5ad
commit 2e51d96416
42 changed files with 396 additions and 240 deletions

View File

@@ -15,6 +15,7 @@ import {
} from "rxjs";
import {
LoginEmailServiceAbstraction,
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
@@ -23,7 +24,6 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
@@ -82,7 +82,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected activatedRoute: ActivatedRoute,
protected messagingService: MessagingService,
protected tokenService: TokenService,
protected loginService: LoginService,
protected loginEmailService: LoginEmailServiceAbstraction,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
@@ -244,23 +244,17 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
return;
}
this.loginService.setEmail(this.data.userEmail);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/login-with-device"]);
this.loginEmailService.setEmail(this.data.userEmail);
await this.router.navigate(["/login-with-device"]);
}
async requestAdminApproval() {
this.loginService.setEmail(this.data.userEmail);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/admin-approval-requested"]);
this.loginEmailService.setEmail(this.data.userEmail);
await this.router.navigate(["/admin-approval-requested"]);
}
async approveWithMasterPassword() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } });
await this.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } });
}
async createUser() {

View File

@@ -1,8 +1,8 @@
import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/password-hint.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -22,11 +22,11 @@ export class HintComponent implements OnInit {
protected apiService: ApiService,
protected platformUtilsService: PlatformUtilsService,
private logService: LogService,
private loginService: LoginService,
private loginEmailService: LoginEmailServiceAbstraction,
) {}
ngOnInit(): void {
this.email = this.loginService.getEmail() ?? "";
this.email = this.loginEmailService.getEmail() ?? "";
}
async submit() {

View File

@@ -6,12 +6,12 @@ import {
AuthRequestLoginCredentials,
AuthRequestServiceAbstraction,
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
@@ -83,7 +83,7 @@ export class LoginViaAuthRequestComponent
private anonymousHubService: AnonymousHubService,
private validationService: ValidationService,
private stateService: StateService,
private loginService: LoginService,
private loginEmailService: LoginEmailServiceAbstraction,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private loginStrategyService: LoginStrategyServiceAbstraction,
@@ -94,7 +94,7 @@ export class LoginViaAuthRequestComponent
// Why would the existence of the email depend on the navigation?
const navigation = this.router.getCurrentNavigation();
if (navigation) {
this.email = this.loginService.getEmail();
this.email = this.loginEmailService.getEmail();
}
// Gets signalR push notification
@@ -151,7 +151,7 @@ export class LoginViaAuthRequestComponent
} else {
// Standard auth request
// TODO: evaluate if we can remove the setting of this.email in the constructor
this.email = this.loginService.getEmail();
this.email = this.loginEmailService.getEmail();
if (!this.email) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
@@ -472,17 +472,10 @@ export class LoginViaAuthRequestComponent
}
}
async setRememberEmailValues() {
const rememberEmail = this.loginService.getRememberEmail();
const rememberedEmail = this.loginService.getEmail();
await this.stateService.setRememberedEmail(rememberEmail ? rememberedEmail : null);
this.loginService.clearValues();
}
private async handleSuccessfulLoginNavigation() {
if (this.state === State.StandardAuthRequest) {
// Only need to set remembered email on standard login with auth req flow
await this.setRememberEmailValues();
await this.loginEmailService.saveEmailSettings();
}
if (this.onSuccessfulLogin != null) {

View File

@@ -4,9 +4,12 @@ import { ActivatedRoute, Router } from "@angular/router";
import { Subject, firstValueFrom } from "rxjs";
import { take, takeUntil } from "rxjs/operators";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
PasswordLoginCredentials,
} from "@bitwarden/auth/common";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
@@ -77,7 +80,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
protected formBuilder: FormBuilder,
protected formValidationErrorService: FormValidationErrorsService,
protected route: ActivatedRoute,
protected loginService: LoginService,
protected loginEmailService: LoginEmailServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected webAuthnLoginService: WebAuthnLoginServiceAbstraction,
) {
@@ -93,25 +96,23 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
const queryParamsEmail = params.email;
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
this.formGroup.get("email").setValue(queryParamsEmail);
this.loginService.setEmail(queryParamsEmail);
this.formGroup.controls.email.setValue(queryParamsEmail);
this.paramEmailSet = true;
}
});
let email = this.loginService.getEmail();
if (email == null || email === "") {
email = await this.stateService.getRememberedEmail();
}
if (!this.paramEmailSet) {
this.formGroup.get("email")?.setValue(email ?? "");
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
this.formGroup.controls.email.setValue(storedEmail ?? "");
}
let rememberEmail = this.loginService.getRememberEmail();
let rememberEmail = this.loginEmailService.getRememberEmail();
if (rememberEmail == null) {
rememberEmail = (await this.stateService.getRememberedEmail()) != null;
rememberEmail = (await firstValueFrom(this.loginEmailService.storedEmail$)) != null;
}
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
}
ngOnDestroy() {
@@ -148,8 +149,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
this.formPromise = this.loginStrategyService.logIn(credentials);
const response = await this.formPromise;
this.setFormValues();
await this.loginService.saveEmailSettings();
this.setLoginEmailValues();
await this.loginEmailService.saveEmailSettings();
if (this.handleCaptchaRequired(response)) {
return;
} else if (this.handleMigrateEncryptionKey(response)) {
@@ -214,7 +217,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
return;
}
this.setFormValues();
this.setLoginEmailValues();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/login-with-device"]);
@@ -292,14 +295,14 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
}
}
setFormValues() {
this.loginService.setEmail(this.formGroup.value.email);
this.loginService.setRememberEmail(this.formGroup.value.rememberEmail);
setLoginEmailValues() {
this.loginEmailService.setEmail(this.formGroup.value.email);
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
}
async saveEmailSettings() {
this.setFormValues();
await this.loginService.saveEmailSettings();
this.setLoginEmailValues();
await this.loginEmailService.saveEmailSettings();
// Save off email for SSO
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);

View File

@@ -7,14 +7,14 @@ import { BehaviorSubject } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import {
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
@@ -59,7 +59,7 @@ describe("TwoFactorComponent", () => {
let mockLogService: MockProxy<LogService>;
let mockTwoFactorService: MockProxy<TwoFactorService>;
let mockAppIdService: MockProxy<AppIdService>;
let mockLoginService: MockProxy<LoginService>;
let mockLoginEmailService: MockProxy<LoginEmailServiceAbstraction>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockConfigService: MockProxy<ConfigService>;
@@ -89,7 +89,7 @@ describe("TwoFactorComponent", () => {
mockLogService = mock<LogService>();
mockTwoFactorService = mock<TwoFactorService>();
mockAppIdService = mock<AppIdService>();
mockLoginService = mock<LoginService>();
mockLoginEmailService = mock<LoginEmailServiceAbstraction>();
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockConfigService = mock<ConfigService>();
@@ -163,7 +163,7 @@ describe("TwoFactorComponent", () => {
{ provide: LogService, useValue: mockLogService },
{ provide: TwoFactorService, useValue: mockTwoFactorService },
{ provide: AppIdService, useValue: mockAppIdService },
{ provide: LoginService, useValue: mockLoginService },
{ provide: LoginEmailServiceAbstraction, useValue: mockLoginEmailService },
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: mockUserDecryptionOptionsService,
@@ -280,11 +280,11 @@ describe("TwoFactorComponent", () => {
expect(component.onSuccessfulLogin).toHaveBeenCalled();
});
it("calls loginService.clearValues() when login is successful", async () => {
it("calls loginEmailService.clearValues() when login is successful", async () => {
// Arrange
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
// spy on loginService.clearValues
const clearValuesSpy = jest.spyOn(mockLoginService, "clearValues");
// spy on loginEmailService.clearValues
const clearValuesSpy = jest.spyOn(mockLoginEmailService, "clearValues");
// Act
await component.doSubmit();

View File

@@ -8,12 +8,12 @@ import { first } from "rxjs/operators";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import {
LoginStrategyServiceAbstraction,
LoginEmailServiceAbstraction,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
@@ -88,7 +88,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected logService: LogService,
protected twoFactorService: TwoFactorService,
protected appIdService: AppIdService,
protected loginService: LoginService,
protected loginEmailService: LoginEmailServiceAbstraction,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected configService: ConfigService,
@@ -288,7 +288,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
// - TDE login decryption options component
// - Browser SSO on extension open
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
this.loginService.clearValues();
this.loginEmailService.clearValues();
// note: this flow affects both TDE & standard users
if (this.isForcePasswordResetRequired(authResult)) {

View File

@@ -7,6 +7,8 @@ import {
PinCryptoService,
LoginStrategyServiceAbstraction,
LoginStrategyService,
LoginEmailServiceAbstraction,
LoginEmailService,
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
UserDecryptionOptionsServiceAbstraction,
@@ -58,7 +60,6 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
@@ -77,7 +78,6 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { LoginService } from "@bitwarden/common/auth/services/login.service";
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
@@ -874,9 +874,9 @@ const safeProviders: SafeProvider[] = [
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
}),
safeProvider({
provide: LoginServiceAbstraction,
useClass: LoginService,
deps: [StateServiceAbstraction],
provide: LoginEmailServiceAbstraction,
useClass: LoginEmailService,
deps: [StateProvider],
}),
safeProvider({
provide: OrgDomainInternalServiceAbstraction,