mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-11 13:30:39 +01:00
[PM-4917, PM-8707, PM-9119] Persist login email memory through 2fa on browser (#9811)
* persist email memory through 2fa on browser * fix tests * fix desktop
This commit is contained in:
parent
4f3760beae
commit
9c66b5bf9f
@ -1,11 +1,10 @@
|
|||||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { Subject, firstValueFrom, takeUntil } from "rxjs";
|
import { Subject, firstValueFrom, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||||
import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common";
|
import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
@ -34,7 +33,6 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private environmentService: EnvironmentService,
|
|
||||||
private loginEmailService: LoginEmailServiceAbstraction,
|
private loginEmailService: LoginEmailServiceAbstraction,
|
||||||
private accountSwitcherService: AccountSwitcherService,
|
private accountSwitcherService: AccountSwitcherService,
|
||||||
private registerRouteService: RegisterRouteService,
|
private registerRouteService: RegisterRouteService,
|
||||||
@ -55,13 +53,14 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.environmentSelector.onOpenSelfHostedSettings
|
this.environmentSelector.onOpenSelfHostedSettings
|
||||||
.pipe(takeUntil(this.destroyed$))
|
.pipe(
|
||||||
.subscribe(() => {
|
switchMap(async () => {
|
||||||
this.setLoginEmailValues();
|
await this.setLoginEmailValues();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.router.navigate(["environment"]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
}),
|
||||||
this.router.navigate(["environment"]);
|
takeUntil(this.destroyed$),
|
||||||
});
|
)
|
||||||
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -85,12 +84,14 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setLoginEmailValues();
|
await this.setLoginEmailValues();
|
||||||
await this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } });
|
await this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } });
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoginEmailValues() {
|
async setLoginEmailValues() {
|
||||||
this.loginEmailService.setEmail(this.formGroup.value.email);
|
// Note: Browser saves email settings here instead of the login component
|
||||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||||
|
this.loginEmailService.setEmail(this.formGroup.value.email);
|
||||||
|
await this.loginEmailService.saveEmailSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-footer">
|
<div class="box-footer">
|
||||||
<button type="button" class="btn link" routerLink="/hint" (click)="setLoginEmailValues()">
|
<button type="button" class="btn link" routerLink="/hint" (click)="saveEmailSettings()">
|
||||||
<b>{{ "getMasterPasswordHint" | i18n }}</b>
|
<b>{{ "getMasterPasswordHint" | i18n }}</b>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -99,7 +99,7 @@ export class LoginComponent extends BaseLoginComponent {
|
|||||||
async launchSsoBrowser() {
|
async launchSsoBrowser() {
|
||||||
// Save off email for SSO
|
// Save off email for SSO
|
||||||
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||||
await this.loginEmailService.saveEmailSettings();
|
|
||||||
// Generate necessary sso params
|
// Generate necessary sso params
|
||||||
const passwordOptions: any = {
|
const passwordOptions: any = {
|
||||||
type: "password",
|
type: "password",
|
||||||
@ -142,4 +142,9 @@ export class LoginComponent extends BaseLoginComponent {
|
|||||||
encodeURIComponent(this.formGroup.controls.email.value),
|
encodeURIComponent(this.formGroup.controls.email.value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveEmailSettings() {
|
||||||
|
// values should be saved on home component
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@
|
|||||||
class="btn block"
|
class="btn block"
|
||||||
type="button"
|
type="button"
|
||||||
routerLink="/accessibility-cookie"
|
routerLink="/accessibility-cookie"
|
||||||
(click)="setLoginEmailValues()"
|
(click)="saveEmailSettings()"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-universal-access" aria-hidden="true"></i>
|
<i class="bwi bwi-universal-access" aria-hidden="true"></i>
|
||||||
{{ "loadAccessibilityCookie" | i18n }}
|
{{ "loadAccessibilityCookie" | i18n }}
|
||||||
@ -139,7 +139,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="text text-primary password-hint-btn"
|
class="text text-primary password-hint-btn"
|
||||||
routerLink="/hint"
|
routerLink="/hint"
|
||||||
(click)="setLoginEmailValues()"
|
(click)="saveEmailSettings()"
|
||||||
>
|
>
|
||||||
{{ "getMasterPasswordHint" | i18n }}
|
{{ "getMasterPasswordHint" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -75,7 +75,7 @@
|
|||||||
class="tw-mt-2"
|
class="tw-mt-2"
|
||||||
routerLink="/hint"
|
routerLink="/hint"
|
||||||
(mousedown)="goToHint()"
|
(mousedown)="goToHint()"
|
||||||
(click)="setLoginEmailValues()"
|
(click)="saveEmailSettings()"
|
||||||
>{{ "getMasterPasswordHint" | i18n }}</a
|
>{{ "getMasterPasswordHint" | i18n }}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -160,7 +160,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async goToHint() {
|
async goToHint() {
|
||||||
this.setLoginEmailValues();
|
await this.saveEmailSettings();
|
||||||
await this.router.navigateByUrl("/hint");
|
await this.router.navigateByUrl("/hint");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,17 +110,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!this.paramEmailSet) {
|
if (!this.paramEmailSet) {
|
||||||
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
|
await this.loadEmailSettings();
|
||||||
this.formGroup.controls.email.setValue(storedEmail ?? "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let rememberEmail = this.loginEmailService.getRememberEmail();
|
|
||||||
|
|
||||||
if (!rememberEmail) {
|
|
||||||
rememberEmail = (await firstValueFrom(this.loginEmailService.storedEmail$)) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -158,8 +149,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
this.formPromise = this.loginStrategyService.logIn(credentials);
|
this.formPromise = this.loginStrategyService.logIn(credentials);
|
||||||
const response = await this.formPromise;
|
const response = await this.formPromise;
|
||||||
|
|
||||||
this.setLoginEmailValues();
|
await this.saveEmailSettings();
|
||||||
await this.loginEmailService.saveEmailSettings();
|
|
||||||
|
|
||||||
if (this.handleCaptchaRequired(response)) {
|
if (this.handleCaptchaRequired(response)) {
|
||||||
return;
|
return;
|
||||||
@ -181,6 +171,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.onSuccessfulLoginForceResetNavigate();
|
this.onSuccessfulLoginForceResetNavigate();
|
||||||
} else {
|
} else {
|
||||||
|
this.loginEmailService.clearValues();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.router.navigate([this.forcePasswordResetRoute]);
|
this.router.navigate([this.forcePasswordResetRoute]);
|
||||||
@ -196,6 +187,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.onSuccessfulLoginNavigate();
|
this.onSuccessfulLoginNavigate();
|
||||||
} else {
|
} else {
|
||||||
|
this.loginEmailService.clearValues();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.router.navigate([this.successRoute]);
|
this.router.navigate([this.successRoute]);
|
||||||
@ -225,12 +217,14 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setLoginEmailValues();
|
await this.saveEmailSettings();
|
||||||
await this.router.navigate(["/login-with-device"]);
|
await this.router.navigate(["/login-with-device"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||||
await this.saveEmailSettings();
|
// Save off email for SSO
|
||||||
|
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||||
|
|
||||||
// Generate necessary sso params
|
// Generate necessary sso params
|
||||||
const passwordOptions: any = {
|
const passwordOptions: any = {
|
||||||
type: "password",
|
type: "password",
|
||||||
@ -301,17 +295,28 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoginEmailValues() {
|
private async loadEmailSettings() {
|
||||||
this.loginEmailService.setEmail(this.formGroup.value.email);
|
// Try to load from memory first
|
||||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
const email = this.loginEmailService.getEmail();
|
||||||
|
const rememberEmail = this.loginEmailService.getRememberEmail();
|
||||||
|
if (email) {
|
||||||
|
this.formGroup.controls.email.setValue(email);
|
||||||
|
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
|
||||||
|
} else {
|
||||||
|
// If not in memory, check email on disk
|
||||||
|
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
|
||||||
|
if (storedEmail) {
|
||||||
|
// If we have a stored email, rememberEmail should default to true
|
||||||
|
this.formGroup.controls.email.setValue(storedEmail);
|
||||||
|
this.formGroup.controls.rememberEmail.setValue(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveEmailSettings() {
|
protected async saveEmailSettings() {
|
||||||
this.setLoginEmailValues();
|
this.loginEmailService.setEmail(this.formGroup.value.email);
|
||||||
|
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||||
await this.loginEmailService.saveEmailSettings();
|
await this.loginEmailService.saveEmailSettings();
|
||||||
|
|
||||||
// Save off email for SSO
|
|
||||||
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy accounts used the master key to encrypt data. Migration is required
|
// Legacy accounts used the master key to encrypt data. Migration is required
|
||||||
|
@ -137,14 +137,16 @@ describe("LoginEmailService", () => {
|
|||||||
expect(result).toEqual("initialEmail@bitwarden.com");
|
expect(result).toEqual("initialEmail@bitwarden.com");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clears the email and rememberEmail after saving", async () => {
|
it("does not clear the email and rememberEmail after saving", async () => {
|
||||||
|
// Browser uses these values to maintain the email between login and 2fa components so
|
||||||
|
// we do not want to clear them too early.
|
||||||
sut.setEmail("userEmail@bitwarden.com");
|
sut.setEmail("userEmail@bitwarden.com");
|
||||||
sut.setRememberEmail(true);
|
sut.setRememberEmail(true);
|
||||||
await sut.saveEmailSettings();
|
await sut.saveEmailSettings();
|
||||||
|
|
||||||
const result = sut.getEmail();
|
const result = sut.getEmail();
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBe("userEmail@bitwarden.com");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -73,6 +73,9 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
|
|||||||
this.rememberEmail = value ?? false;
|
this.rememberEmail = value ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: only clear values on successful login or you are sure they are not needed.
|
||||||
|
// Browser uses these values to maintain the email between login and 2fa components so
|
||||||
|
// we do not want to clear them too early.
|
||||||
clearValues() {
|
clearValues() {
|
||||||
this.email = null;
|
this.email = null;
|
||||||
this.rememberEmail = false;
|
this.rememberEmail = false;
|
||||||
@ -89,12 +92,11 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
|
|||||||
return storedEmail;
|
return storedEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging in with rememberEmail set to false will clear the stored email
|
// Saving with rememberEmail set to false will clear the stored email
|
||||||
if (this.rememberEmail) {
|
if (this.rememberEmail) {
|
||||||
return this.email;
|
return this.email;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
this.clearValues();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user